first commit

This commit is contained in:
Roman Pyrih
2026-03-10 09:50:10 +01:00
commit 64c4a90405
7289 changed files with 2645777 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Core\Base\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* TODO: Move this class to pro version for better architecture.
*/
class Canary_Deployment extends Module {
const CURRENT_VERSION = ELEMENTOR_VERSION;
const PLUGIN_BASE = ELEMENTOR_PLUGIN_BASE;
private $canary_deployment_info = null;
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 2.6.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'canary-deployment';
}
/**
* Check version.
*
* @since 2.6.0
* @access public
*
* @param object $transient Plugin updates data.
*
* @return object Plugin updates data.
*/
public function check_version( $transient ) {
// First transient before the real check.
if ( ! isset( $transient->response ) ) {
return $transient;
}
// Placeholder
$stable_version = '0.0.0';
if ( ! empty( $transient->response[ static::PLUGIN_BASE ]->new_version ) ) {
$stable_version = $transient->response[ static::PLUGIN_BASE ]->new_version;
}
if ( null === $this->canary_deployment_info ) {
$this->canary_deployment_info = $this->get_canary_deployment_info();
}
// Can be false - if canary version is not available.
if ( empty( $this->canary_deployment_info ) ) {
return $transient;
}
if ( ! version_compare( $this->canary_deployment_info['new_version'], $stable_version, '>' ) ) {
return $transient;
}
$canary_deployment_info = $this->canary_deployment_info;
// Most of plugin info comes from the $transient but on first check - the response is empty.
if ( ! empty( $transient->response[ static::PLUGIN_BASE ] ) ) {
$canary_deployment_info = array_merge( (array) $transient->response[ static::PLUGIN_BASE ], $canary_deployment_info );
}
$transient->response[ static::PLUGIN_BASE ] = (object) $canary_deployment_info;
return $transient;
}
protected function get_canary_deployment_remote_info( $force ) {
return Api::get_canary_deployment_info( $force );
}
private function get_canary_deployment_info() {
global $pagenow;
$force = 'update-core.php' === $pagenow && isset( $_GET['force-check'] );
$canary_deployment = $this->get_canary_deployment_remote_info( $force );
if ( empty( $canary_deployment['plugin_info']['new_version'] ) ) {
return false;
}
$canary_version = $canary_deployment['plugin_info']['new_version'];
if ( version_compare( $canary_version, static::CURRENT_VERSION, '<=' ) ) {
return false;
}
if ( ! empty( $canary_deployment['conditions'] ) && ! $this->check_conditions( $canary_deployment['conditions'] ) ) {
return false;
}
return $canary_deployment['plugin_info'];
}
private function check_conditions( $groups ) {
foreach ( $groups as $group ) {
if ( $this->check_group( $group ) ) {
return true;
}
}
return false;
}
private function check_group( $group ) {
$is_or_relation = ! empty( $group['relation'] ) && 'OR' === $group['relation'];
unset( $group['relation'] );
$result = false;
foreach ( $group as $condition ) {
// Reset results for each condition.
$result = false;
switch ( $condition['type'] ) {
case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
// include an unmodified $wp_version
include ABSPATH . WPINC . '/version.php';
$result = version_compare( $wp_version, $condition['version'], $condition['operator'] );
break;
case 'multisite':
$result = is_multisite() === $condition['multisite'];
break;
case 'language':
$in_array = in_array( get_locale(), $condition['languages'], true );
$result = 'in' === $condition['operator'] ? $in_array : ! $in_array;
break;
case 'plugin':
if ( ! empty( $condition['plugin_file'] ) ) {
$plugin_file = $condition['plugin_file']; // For PHP Unit tests.
} else {
$plugin_file = WP_PLUGIN_DIR . '/' . $condition['plugin']; // Default.
}
$version = '';
if ( is_plugin_active( $condition['plugin'] ) && file_exists( $plugin_file ) ) {
$plugin_data = get_plugin_data( $plugin_file );
if ( isset( $plugin_data['Version'] ) ) {
$version = $plugin_data['Version'];
}
}
$result = version_compare( $version, $condition['version'], $condition['operator'] );
break;
case 'theme':
$theme = wp_get_theme();
if ( wp_get_theme()->parent() ) {
$theme = wp_get_theme()->parent();
}
if ( $theme->get_template() === $condition['theme'] ) {
$version = $theme->version;
} else {
$version = '';
}
$result = version_compare( $version, $condition['version'], $condition['operator'] );
break;
}
if ( ( $is_or_relation && $result ) || ( ! $is_or_relation && ! $result ) ) {
return $result;
}
}
return $result;
}
/**
* @since 2.6.0
* @access public
*/
public function __construct() {
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_version' ] );
}
}

View File

@@ -0,0 +1,348 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu;
use Elementor\Core\Admin\EditorOneMenu\Menu\Editor_One_Custom_Elements_Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Modules\EditorOne\Classes\Legacy_Submenu_Interceptor;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
use Elementor\Modules\EditorOne\Classes\Menu_Data_Provider;
use Elementor\Modules\EditorOne\Classes\Slug_Normalizer;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Elementor_One_Menu_Manager {
private Menu_Data_Provider $menu_data_provider;
private bool $is_pro_module_enabled = false;
private Legacy_Submenu_Interceptor $legacy_submenu_interceptor;
public function __construct() {
$this->menu_data_provider = Menu_Data_Provider::instance();
$this->legacy_submenu_interceptor = new Legacy_Submenu_Interceptor(
$this->menu_data_provider,
new Slug_Normalizer()
);
$this->register_actions();
}
private function register_actions(): void {
add_action( 'init', [ $this, 'check_if_pro_module_is_enabled' ] );
add_action( 'admin_menu', [ $this, 'register_elementor_home_submenus' ], 9 );
add_action( 'admin_menu', function () {
do_action( 'elementor/editor-one/menu/register', $this->menu_data_provider );
} );
add_action( 'admin_menu', [ $this, 'register_pro_submenus' ], 100 );
add_action( 'admin_menu', [ $this, 'intercept_legacy_submenus' ], 10003 );
add_action( 'admin_menu', [ $this, 'register_flyout_items_as_hidden_submenus' ], 10004 );
add_action( 'admin_menu', [ $this, 'remove_all_submenus_for_edit_posts_users' ], 10005 );
add_action( 'admin_menu', [ $this, 'override_elementor_page_for_edit_posts_users' ], 1006 );
add_filter( 'add_menu_classes', [ $this, 'fix_theme_builder_submenu_url' ] );
add_action( 'admin_head', [ $this, 'hide_flyout_items_from_wp_menu' ] );
add_action( 'admin_head', [ $this, 'hide_legacy_templates_menu' ] );
add_action( 'admin_head', [ $this, 'hide_old_elementor_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_menu_assets' ] );
add_action( 'admin_print_scripts-elementor_page_elementor-editor', [ $this, 'enqueue_home_screen_on_editor_page' ] );
}
public function check_if_pro_module_is_enabled(): void {
$this->is_pro_module_enabled = apply_filters( 'elementor/modules/editor-one/is_pro_module_enabled', false );
if ( ! $this->is_pro_module_enabled && Utils::has_pro() ) {
$this->menu_data_provider->register_menu( new Editor_One_Custom_Elements_Menu() );
}
}
public function register_elementor_home_submenus(): void {
add_submenu_page(
Menu_Config::ELEMENTOR_HOME_MENU_SLUG,
esc_html__( 'Editor', 'elementor' ),
esc_html__( 'Editor', 'elementor' ),
Menu_Config::CAPABILITY_EDIT_POSTS,
Menu_Config::ELEMENTOR_MENU_SLUG,
[ $this, 'render_editor_page' ],
20
);
do_action( 'elementor/editor-one/menu/register_submenus' );
}
public function register_pro_submenus(): void {
if ( ! $this->is_pro_module_enabled &&
Utils::has_pro() &&
class_exists( '\ElementorPro\License\API' ) &&
\ElementorPro\License\API::is_license_active()
) {
add_submenu_page(
Menu_Config::ELEMENTOR_HOME_MENU_SLUG,
esc_html__( 'Theme Builder', 'elementor' ),
esc_html__( 'Theme Builder', 'elementor' ),
Menu_Config::CAPABILITY_EDIT_POSTS,
'elementor-theme-builder',
'',
70
);
add_submenu_page(
Menu_Config::ELEMENTOR_HOME_MENU_SLUG,
esc_html__( 'Submissions', 'elementor' ),
esc_html__( 'Submissions', 'elementor' ),
'edit_posts',
'e-form-submissions',
'',
80
);
}
}
public function remove_all_submenus_for_edit_posts_users(): void {
$user_capabilities = Menu_Data_Provider::get_current_user_capabilities();
if ( ! $user_capabilities['is_edit_posts_user'] ) {
return;
}
global $submenu;
if ( empty( $submenu[ Menu_Config::ELEMENTOR_MENU_SLUG ] ) ) {
return;
}
$submenu_items = $submenu[ Menu_Config::ELEMENTOR_MENU_SLUG ];
foreach ( $submenu_items as $index => $submenu_item ) {
if ( 0 === $index ) {
continue;
}
$submenu_slug = $submenu_item[2] ?? '';
if ( ! empty( $submenu_slug ) ) {
remove_submenu_page( Menu_Config::ELEMENTOR_MENU_SLUG, $submenu_slug );
}
}
}
public function render_editor_page(): void {
Plugin::instance()->settings->display_home_screen();
}
public function override_elementor_page_for_edit_posts_users(): void {
$user_capabilities = Menu_Data_Provider::get_current_user_capabilities();
if ( ! $user_capabilities['is_edit_posts_user'] ) {
return;
}
$page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
if ( Menu_Config::ELEMENTOR_MENU_SLUG !== $page ) {
return;
}
$templates_url = admin_url( 'edit.php?post_type=elementor_library&tabs_group=library' );
wp_safe_redirect( $templates_url );
exit;
}
public function enqueue_home_screen_on_editor_page(): void {
$home_module = Plugin::instance()->modules_manager->get_modules( 'home' );
if ( $home_module && method_exists( $home_module, 'enqueue_home_screen_scripts' ) ) {
$home_module->enqueue_home_screen_scripts();
}
}
public function fix_theme_builder_submenu_url( $menu ) {
global $submenu;
$menu_slugs = [ Menu_Config::ELEMENTOR_HOME_MENU_SLUG ];
foreach ( $menu_slugs as $menu_slug ) {
if ( empty( $submenu[ $menu_slug ] ) ) {
continue;
}
foreach ( $submenu[ $menu_slug ] as &$item ) {
if ( 'elementor-theme-builder' === $item[2] ) {
$item[2] = $this->get_theme_builder_url(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
break;
}
}
}
return $menu;
}
private function get_theme_builder_url(): string {
return $this->menu_data_provider->get_theme_builder_url();
}
public function hide_legacy_templates_menu(): void {
?>
<style type="text/css">
#menu-posts-elementor_library {
display: none !important;
}
</style>
<?php
}
public function hide_old_elementor_menu(): void {
$this->remove_elementor_separator();
?>
<style type="text/css">
#toplevel_page_elementor {
display: none !important;
}
</style>
<?php
}
private function remove_elementor_separator(): void {
global $menu;
foreach ( $menu as $key => $item ) {
if ( isset( $item[2] ) && 'separator-elementor' === $item[2] ) {
unset( $menu[ $key ] );
break;
}
}
}
public function register_flyout_items_as_hidden_submenus(): void {
$hooks = [];
$this->iterate_all_flyout_items( function( string $item_slug, Menu_Item_Interface $item ) use ( &$hooks ) {
$hook = $this->register_hidden_submenu( $item_slug, $item );
if ( $hook ) {
$hooks[ $item_slug ] = $hook;
}
} );
do_action( 'elementor/editor-one/menu/after_register_hidden_submenus', $hooks );
}
private function register_hidden_submenu( string $item_slug, Menu_Item_Interface $item ) {
$original_parent = $this->get_original_parent_slug( $item );
$parent_slug = $this->resolve_hidden_submenu_parent( $original_parent );
$has_page = method_exists( $item, 'render' );
$page_title = $has_page ? $item->get_page_title() : '';
$callback = $has_page ? [ $item, 'render' ] : '';
$capability = $item->get_capability();
$position = $item->get_position();
return add_submenu_page(
$parent_slug,
$page_title,
$item->get_label(),
$capability,
$item_slug,
$callback,
$position
);
}
private function resolve_hidden_submenu_parent( ?string $parent_slug ): string {
$default_parent_slug = Menu_Config::ELEMENTOR_HOME_MENU_SLUG;
if ( empty( $parent_slug ) ) {
return $default_parent_slug;
}
$elementor_parent_slugs = [
Menu_Config::EDITOR_GROUP_ID => true,
Menu_Config::EDITOR_MENU_SLUG => true,
Menu_Config::TEMPLATES_GROUP_ID => true,
Menu_Config::LEGACY_TEMPLATES_SLUG => true,
Menu_Config::SETTINGS_GROUP_ID => true,
Menu_Config::CUSTOM_ELEMENTS_GROUP_ID => true,
Menu_Config::SYSTEM_GROUP_ID => true,
];
if ( isset( $elementor_parent_slugs[ $parent_slug ] ) ) {
return $default_parent_slug;
}
return $parent_slug;
}
private function iterate_all_flyout_items( callable $callback ): void {
$level3_items = $this->menu_data_provider->get_level3_items();
$level4_items = $this->menu_data_provider->get_level4_items();
$all_items = array_merge_recursive( $level3_items, $level4_items );
foreach ( $all_items as $group_items ) {
foreach ( $group_items as $item_slug => $item ) {
$callback( $item_slug, $item );
}
}
}
private function get_original_parent_slug( $item ): ?string {
return $item->get_parent_slug();
}
public function hide_flyout_items_from_wp_menu(): void {
$protected_wp_menu_slugs = [
Menu_Config::EDITOR_MENU_SLUG,
'elementor-theme-builder',
'e-form-submissions',
];
$this->iterate_all_flyout_items( function( string $item_slug, Menu_Item_Interface $item ) use ( $protected_wp_menu_slugs ) {
if ( in_array( $item_slug, $protected_wp_menu_slugs, true ) ) {
return;
}
$original_parent = $this->get_original_parent_slug( $item );
$parent_slug = $this->resolve_hidden_submenu_parent( $original_parent );
remove_submenu_page( $parent_slug, $item_slug );
} );
}
public function intercept_legacy_submenus(): void {
$this->legacy_submenu_interceptor->intercept_all( $this->is_pro_module_enabled );
}
public function enqueue_admin_menu_assets(): void {
$min_suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_style(
'elementor-admin-menu',
ELEMENTOR_ASSETS_URL . 'css/modules/editor-one/admin-menu' . $min_suffix . '.css',
[],
ELEMENTOR_VERSION
);
$config = [
'editorFlyout' => $this->menu_data_provider->get_third_level_data(
Menu_Data_Provider::THIRD_LEVEL_FLYOUT_MENU
),
];
wp_enqueue_script(
'editor-one-menu',
ELEMENTOR_ASSETS_URL . 'js/editor-one-menu' . $min_suffix . '.js',
[],
ELEMENTOR_VERSION,
true
);
wp_localize_script(
'editor-one-menu',
'editorOneMenuConfig',
$config
);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
interface Menu_Item_Interface {
public function get_capability(): string;
public function get_label(): string;
public function get_parent_slug(): string;
public function is_visible(): bool;
public function get_position(): int;
public function get_slug(): string;
public function get_group_id(): string;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
interface Menu_Item_Third_Level_Interface extends Menu_Item_Interface {
public function get_icon(): string;
public function has_children(): bool;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
interface Menu_Item_With_Custom_Url_Interface {
public function get_menu_url(): string;
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class Abstract_Level4_Menu_Item implements Menu_Item_Interface {
public function get_capability(): string {
return Menu_Config::CAPABILITY_MANAGE_OPTIONS;
}
public function get_parent_slug(): string {
return Menu_Config::ELEMENTOR_MENU_SLUG;
}
public function is_visible(): bool {
return true;
}
abstract public function get_label(): string;
abstract public function get_position(): int;
abstract public function get_slug(): string;
abstract public function get_group_id(): string;
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Third_Level_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class Abstract_Menu_Item implements Menu_Item_Third_Level_Interface {
public function get_capability(): string {
return Menu_Config::CAPABILITY_MANAGE_OPTIONS;
}
public function get_parent_slug(): string {
return Menu_Config::ELEMENTOR_MENU_SLUG;
}
public function is_visible(): bool {
return true;
}
public function get_group_id(): string {
return Menu_Config::EDITOR_GROUP_ID;
}
public function has_children(): bool {
return false;
}
abstract public function get_label(): string;
abstract public function get_position(): int;
abstract public function get_slug(): string;
abstract public function get_icon(): string;
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Third_Level_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Editor_One_Custom_Elements_Menu implements Menu_Item_Third_Level_Interface {
public function get_capability(): string {
return Menu_Config::CAPABILITY_MANAGE_OPTIONS;
}
public function get_parent_slug(): string {
return Menu_Config::ELEMENTOR_MENU_SLUG;
}
public function is_visible(): bool {
return true;
}
public function get_label(): string {
return esc_html__( 'Custom Elements', 'elementor' );
}
public function get_position(): int {
return 70;
}
public function get_slug(): string {
return 'elementor-custom-elements';
}
public function get_icon(): string {
return 'adjustments';
}
public function get_group_id(): string {
return Menu_Config::CUSTOM_ELEMENTS_GROUP_ID;
}
public function has_children(): bool {
return true;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Legacy_Submenu_Item_Not_Mapped implements Menu_Item_Interface {
private $submenu_data;
private $parent_slug;
private $position;
public function __construct( array $submenu_data, ?string $parent_slug = null, ?int $position = 100 ) {
$this->submenu_data = $submenu_data;
$this->parent_slug = $parent_slug ?? Menu_Config::ELEMENTOR_MENU_SLUG;
$this->position = $position;
}
public function get_label(): string {
return $this->submenu_data[0] ?? '';
}
public function get_capability(): string {
return $this->submenu_data[1] ?? Menu_Config::CAPABILITY_MANAGE_OPTIONS;
}
public function get_slug(): string {
return $this->submenu_data[2] ?? '';
}
public function get_parent_slug(): string {
return $this->parent_slug;
}
public function is_visible(): bool {
return true;
}
public function get_page_title(): string {
return $this->submenu_data[3] ?? $this->get_label();
}
public function get_position(): int {
return $this->position;
}
public function get_group_id(): string {
return Menu_Config::THIRD_PARTY_GROUP_ID;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Third_Level_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Legacy_Submenu_Item implements Menu_Item_Third_Level_Interface {
private $submenu_data;
private $parent_slug;
private $position;
public function __construct( array $submenu_data, ?string $parent_slug = null, ?int $position = 100 ) {
$this->submenu_data = $submenu_data;
$this->parent_slug = $parent_slug ?? Menu_Config::ELEMENTOR_MENU_SLUG;
$this->position = $position;
}
public function get_label(): string {
return $this->submenu_data[0] ?? '';
}
public function get_capability(): string {
return $this->submenu_data[1] ?? Menu_Config::CAPABILITY_MANAGE_OPTIONS;
}
public function get_slug(): string {
return $this->submenu_data[2] ?? '';
}
public function get_parent_slug(): string {
return $this->parent_slug;
}
public function is_visible(): bool {
return true;
}
public function get_page_title(): string {
return $this->submenu_data[3] ?? $this->get_label();
}
public function get_position(): int {
return $this->position;
}
public function get_group_id(): string {
return $this->submenu_data[4] ?? Menu_Config::EDITOR_GROUP_ID;
}
public function get_icon(): string {
$item_slug = $this->get_slug();
$icon = Menu_Config::get_attribute_mapping()[ $item_slug ]['icon'] ?? 'admin-generic';
return $icon;
}
public function has_children(): bool {
return false;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\Core\Admin\EditorOneMenu\Menu;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Third_Level_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Third_Party_Pages_Menu implements Menu_Item_Third_Level_Interface {
public function get_capability(): string {
return Menu_Config::CAPABILITY_EDIT_POSTS;
}
public function get_parent_slug(): string {
return Menu_Config::ELEMENTOR_HOME_MENU_SLUG;
}
public function is_visible(): bool {
return true;
}
public function get_label(): string {
return esc_html__( 'Addons', 'elementor' );
}
public function get_position(): int {
return 200;
}
public function get_slug(): string {
return 'elementor-third-party-pages';
}
public function get_icon(): string {
return 'extension';
}
public function get_group_id(): string {
return Menu_Config::THIRD_PARTY_GROUP_ID;
}
public function has_children(): bool {
return true;
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Core\Base\Module;
use Elementor\Plugin;
use Elementor\Tracker;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Feedback extends Module {
/**
* @since 2.2.0
* @access public
*/
public function __construct() {
add_action( 'current_screen', function () {
if ( ! $this->is_plugins_screen() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_feedback_dialog_scripts' ] );
} );
// Ajax.
add_action( 'wp_ajax_elementor_deactivate_feedback', [ $this, 'ajax_elementor_deactivate_feedback' ] );
}
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'feedback';
}
/**
* Enqueue feedback dialog scripts.
*
* Registers the feedback dialog scripts and enqueues them.
*
* @since 1.0.0
* @access public
*/
public function enqueue_feedback_dialog_scripts() {
add_action( 'admin_footer', [ $this, 'print_deactivate_feedback_dialog' ] );
$suffix = Utils::is_script_debug() ? '' : '.min';
wp_register_script(
'elementor-admin-feedback',
ELEMENTOR_ASSETS_URL . 'js/admin-feedback' . $suffix . '.js',
[
'elementor-common',
'wp-i18n',
],
ELEMENTOR_VERSION,
true
);
wp_enqueue_script( 'elementor-admin-feedback' );
wp_set_script_translations( 'elementor-admin-feedback', 'elementor' );
}
/**
* Print deactivate feedback dialog.
*
* Display a dialog box to ask the user why he deactivated Elementor.
*
* Fired by `admin_footer` filter.
*
* @since 1.0.0
* @access public
*/
public function print_deactivate_feedback_dialog() {
$deactivate_reasons = [
'no_longer_needed' => [
'title' => esc_html__( 'I no longer need the plugin', 'elementor' ),
'input_placeholder' => '',
],
'found_a_better_plugin' => [
'title' => esc_html__( 'I found a better plugin', 'elementor' ),
'input_placeholder' => esc_html__( 'Please share which plugin', 'elementor' ),
],
'couldnt_get_the_plugin_to_work' => [
'title' => esc_html__( 'I couldn\'t get the plugin to work', 'elementor' ),
'input_placeholder' => '',
],
'temporary_deactivation' => [
'title' => esc_html__( 'It\'s a temporary deactivation', 'elementor' ),
'input_placeholder' => '',
],
'elementor_pro' => [
'title' => esc_html__( 'I have Elementor Pro', 'elementor' ),
'input_placeholder' => '',
'alert' => esc_html__( 'Wait! Don\'t deactivate Elementor. You have to activate both Elementor and Elementor Pro in order for the plugin to work.', 'elementor' ),
],
'other' => [
'title' => esc_html__( 'Other', 'elementor' ),
'input_placeholder' => esc_html__( 'Please share the reason', 'elementor' ),
],
];
?>
<div id="elementor-deactivate-feedback-dialog-wrapper">
<div id="elementor-deactivate-feedback-dialog-header">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<span id="elementor-deactivate-feedback-dialog-header-title"><?php echo esc_html__( 'Quick Feedback', 'elementor' ); ?></span>
</div>
<form id="elementor-deactivate-feedback-dialog-form" method="post">
<?php
wp_nonce_field( '_elementor_deactivate_feedback_nonce' );
?>
<input type="hidden" name="action" value="elementor_deactivate_feedback" />
<div id="elementor-deactivate-feedback-dialog-form-caption"><?php echo esc_html__( 'If you have a moment, please share why you are deactivating Elementor:', 'elementor' ); ?></div>
<div id="elementor-deactivate-feedback-dialog-form-body">
<?php foreach ( $deactivate_reasons as $reason_key => $reason ) : ?>
<div class="elementor-deactivate-feedback-dialog-input-wrapper">
<input id="elementor-deactivate-feedback-<?php echo esc_attr( $reason_key ); ?>" class="elementor-deactivate-feedback-dialog-input" type="radio" name="reason_key" value="<?php echo esc_attr( $reason_key ); ?>" />
<label for="elementor-deactivate-feedback-<?php echo esc_attr( $reason_key ); ?>" class="elementor-deactivate-feedback-dialog-label"><?php echo esc_html( $reason['title'] ); ?></label>
<?php if ( ! empty( $reason['input_placeholder'] ) ) : ?>
<input class="elementor-feedback-text" type="text" name="reason_<?php echo esc_attr( $reason_key ); ?>" placeholder="<?php echo esc_attr( $reason['input_placeholder'] ); ?>" />
<?php endif; ?>
<?php if ( ! empty( $reason['alert'] ) ) : ?>
<div class="elementor-feedback-text"><?php echo esc_html( $reason['alert'] ); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</form>
</div>
<?php
}
/**
* Ajax elementor deactivate feedback.
*
* Send the user feedback when Elementor is deactivated.
*
* Fired by `wp_ajax_elementor_deactivate_feedback` action.
*
* @since 1.0.0
* @access public
*/
public function ajax_elementor_deactivate_feedback() {
$wpnonce = Utils::get_super_global_value( $_POST, '_wpnonce' ); // phpcs:ignore -- Nonce verification is made in `wp_verify_nonce()`.
if ( ! wp_verify_nonce( $wpnonce, '_elementor_deactivate_feedback_nonce' ) ) {
wp_send_json_error();
}
if ( ! current_user_can( 'activate_plugins' ) ) {
wp_send_json_error( 'Permission denied' );
}
$reason_key = Utils::get_super_global_value( $_POST, 'reason_key' ) ?? '';
$reason_text = Utils::get_super_global_value( $_POST, "reason_{$reason_key}" ) ?? '';
Api::send_feedback( $reason_key, $reason_text );
wp_send_json_success();
}
/**
* @since 2.3.0
* @access private
*/
private function is_plugins_screen() {
return in_array( get_current_screen()->id, [ 'plugins', 'plugins-network' ] );
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Elementor\Core\Admin\Menu;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_Has_Position;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Menu_Manager {
/**
* @var Admin_Menu_Item[]
*/
private $items = [];
public function register( $item_slug, Admin_Menu_Item $item ) {
$this->items[ $item_slug ] = $item;
}
public function unregister( $item_slug ) {
unset( $this->items[ $item_slug ] );
}
public function get( $item_slug ) {
if ( empty( $this->items[ $item_slug ] ) ) {
return null;
}
return $this->items[ $item_slug ];
}
public function get_all() {
return $this->items;
}
public function register_actions() {
add_action( 'admin_menu', function () {
$this->register_wp_menus();
}, 20 );
add_action( 'admin_head', function () {
$this->hide_invisible_menus();
} );
}
private function register_wp_menus() {
do_action( 'elementor/admin/menu/register', $this );
$hooks = [];
foreach ( $this->get_all() as $item_slug => $item ) {
$is_top_level = empty( $item->get_parent_slug() );
if ( $is_top_level ) {
$hooks[ $item_slug ] = $this->register_top_level_menu( $item_slug, $item );
} else {
$hooks[ $item_slug ] = $this->register_sub_menu( $item_slug, $item );
}
}
do_action( 'elementor/admin/menu/after_register', $this, $hooks );
}
private function register_top_level_menu( $item_slug, Admin_Menu_Item $item ) {
$has_page = ( $item instanceof Admin_Menu_Item_With_Page );
$has_position = ( $item instanceof Admin_Menu_Item_Has_Position );
$page_title = $has_page ? $item->get_page_title() : '';
$callback = $has_page ? [ $item, 'render' ] : '';
$position = $has_position ? $item->get_position() : null;
return add_menu_page(
$page_title,
$item->get_label(),
$item->get_capability(),
$item_slug,
$callback,
'',
$position
);
}
private function register_sub_menu( $item_slug, Admin_Menu_Item $item ) {
$has_page = ( $item instanceof Admin_Menu_Item_With_Page );
$page_title = $has_page ? $item->get_page_title() : '';
$callback = $has_page ? [ $item, 'render' ] : '';
return add_submenu_page(
$item->get_parent_slug(),
$page_title,
$item->get_label(),
$item->get_capability(),
$item_slug,
$callback
);
}
private function hide_invisible_menus() {
foreach ( $this->get_all() as $item_slug => $item ) {
if ( $item->is_visible() ) {
continue;
}
$is_top_level = empty( $item->get_parent_slug() );
if ( $is_top_level ) {
remove_menu_page( $item_slug );
} else {
remove_submenu_page( $item->get_parent_slug(), $item_slug );
}
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Elementor\Core\Admin\Menu;
use Elementor\Core\Base\Base_Object;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base extends Base_Object {
private $args;
private $options;
private $submenus = [];
abstract protected function get_init_args();
public function __construct() {
$this->init_args();
$this->init_options();
add_action( 'admin_menu', function() {
$this->register();
} );
if ( $this->options['separator'] ) {
add_action( 'admin_menu', function() {
$this->add_menu_separator();
} );
add_filter( 'custom_menu_order', '__return_true' );
add_filter( 'menu_order', function( $menu_order ) {
return $this->rearrange_menu_separator( $menu_order );
} );
}
}
public function get_args( $arg = null ) {
return self::get_items( $this->args, $arg );
}
public function add_submenu( $submenu_args ) {
$default_submenu_args = [
'page_title' => '',
'capability' => $this->args['capability'],
'function' => null,
'index' => null,
];
$this->submenus[] = array_merge( $default_submenu_args, $submenu_args );
}
protected function get_init_options() {
return [];
}
protected function register_default_submenus() {}
protected function register() {
$args = $this->args;
add_menu_page( $args['page_title'], $args['menu_title'], $args['capability'], $args['menu_slug'], $args['function'], $args['icon_url'], $args['position'] );
$this->register_default_submenus();
do_action( 'elementor/admin/menu_registered/' . $args['menu_slug'], $this );
usort( $this->submenus, function( $a, $b ) {
return $a['index'] - $b['index'];
} );
foreach ( $this->submenus as $index => $submenu_item ) {
$submenu_args = [
$args['menu_slug'],
$submenu_item['page_title'],
$submenu_item['menu_title'],
$submenu_item['capability'],
$submenu_item['menu_slug'],
$submenu_item['function'],
];
if ( 0 === $submenu_item['index'] ) {
$submenu_args[] = 0;
}
add_submenu_page( ...$submenu_args );
if ( ! empty( $submenu_item['class'] ) ) {
global $submenu;
$submenu[ $args['menu_slug'] ][ $index + 1 ][4] = $submenu_item['class']; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
}
}
private function init_args() {
$default_args = [
'function' => null,
'icon_url' => null,
'position' => null,
];
$this->args = array_merge( $default_args, $this->get_init_args() );
}
private function init_options() {
$default_options = [
'separator' => false,
];
$this->options = array_merge( $default_options, $this->get_init_options() );
}
private function add_menu_separator() {
global $menu;
$slug = $this->args['menu_slug'];
$menu[] = [ '', 'read', 'separator-' . $slug, '', 'wp-menu-separator ' . $slug ]; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
private function rearrange_menu_separator( $menu_order ) {
// Initialize our custom order array.
$custom_menu_order = [];
$slug = $this->args['menu_slug'];
$separator_name = 'separator-' . $slug;
// Get the index of our custom separator.
$custom_separator = array_search( $separator_name, $menu_order, true );
// Loop through menu order and do some rearranging.
foreach ( $menu_order as $item ) {
if ( $slug === $item ) {
$custom_menu_order[] = $separator_name;
$custom_menu_order[] = $item;
unset( $menu_order[ $custom_separator ] );
} elseif ( $separator_name !== $item ) {
$custom_menu_order[] = $item;
}
}
// Return order.
return $custom_menu_order;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Elementor\Core\Admin\Menu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Admin_Menu_Item_Has_Position {
public function get_position();
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Elementor\Core\Admin\Menu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Admin_Menu_Item_With_Page extends Admin_Menu_Item {
public function get_page_title();
public function render();
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Elementor\Core\Admin\Menu\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Admin_Menu_Item {
public function get_capability();
public function get_label();
public function get_parent_slug();
public function is_visible();
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Elementor\Core\Admin\Menu;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Tools;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Main extends Base {
protected function get_init_args() {
return [
'page_title' => esc_html__( 'Elementor', 'elementor' ),
'menu_title' => esc_html__( 'Elementor', 'elementor' ),
'capability' => 'manage_options',
'menu_slug' => 'elementor',
'function' => [ Plugin::$instance->settings, 'display_settings_page' ],
'position' => 58.5,
];
}
protected function get_init_options() {
return [
'separator' => true,
];
}
protected function register_default_submenus() {
$this->add_submenu( [
'page_title' => esc_html_x( 'Templates', 'Template Library', 'elementor' ),
'menu_title' => esc_html_x( 'Templates', 'Template Library', 'elementor' ),
'menu_slug' => Source_Local::ADMIN_MENU_SLUG,
'index' => 0,
] );
$this->add_submenu( [
'menu_title' => esc_html__( 'Help', 'elementor' ),
'menu_slug' => 'go_knowledge_base_site',
'function' => [ Plugin::$instance->settings, 'handle_external_redirects' ],
'index' => 150,
] );
}
protected function register() {
parent::register();
$this->rearrange_elementor_submenu();
}
private function rearrange_elementor_submenu() {
global $submenu;
$elementor_menu_slug = $this->get_args( 'menu_slug' );
$elementor_submenu_old_index = null;
$tools_submenu_index = null;
foreach ( $submenu[ $elementor_menu_slug ] as $index => $submenu_item ) {
if ( $elementor_menu_slug === $submenu_item[2] ) {
$elementor_submenu_old_index = $index;
} elseif ( Tools::PAGE_ID === $submenu_item[2] ) {
$tools_submenu_index = $index;
break;
}
}
$elementor_submenu = array_splice( $submenu[ $elementor_menu_slug ], $elementor_submenu_old_index, 1 );
$elementor_submenu[0][0] = esc_html__( 'Settings', 'elementor' );
array_splice( $submenu[ $elementor_menu_slug ], $tools_submenu_index, 0, $elementor_submenu );
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Core\Admin\Notices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Notice {
/**
* Determine if the notice should be printed or not.
*
* @return boolean
*/
abstract public function should_print();
/**
* Returns the config of the notice itself.
* based on that config the notice will be printed.
*
* @see \Elementor\Core\Admin\Admin_Notices::admin_notices
*
* @return array
*/
abstract public function get_config();
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Elementor\Core\Admin\UI\Components;
use Elementor\Core\Base\Base_Object;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Button extends Base_Object {
private $options;
/**
* @inheritDoc
*/
public function get_name() {
return 'admin-button';
}
public function print_button() {
$options = $this->get_options();
if ( empty( $options['text'] ) ) {
return;
}
$html_tag = ! empty( $options['url'] ) ? 'a' : 'button';
$before = '';
$icon = '';
$attributes = [];
if ( ! empty( $options['icon'] ) ) {
$icon = '<i class="' . esc_attr( $options['icon'] ) . '"></i>';
}
$classes = $options['classes'];
$default_classes = $this->get_default_options( 'classes' );
$classes = array_merge( $classes, $default_classes );
if ( ! empty( $options['type'] ) ) {
$classes[] = 'e-button--' . $options['type'];
}
if ( ! empty( $options['variant'] ) ) {
$classes[] = 'e-button--' . $options['variant'];
}
if ( ! empty( $options['before'] ) ) {
$before = '<span>' . wp_kses_post( $options['before'] ) . '</span>';
}
if ( ! empty( $options['url'] ) ) {
$attributes['href'] = $options['url'];
if ( $options['new_tab'] ) {
$attributes['target'] = '_blank';
}
}
$attributes['class'] = $classes;
$html = $before . '<' . $html_tag . ' ' . Utils::render_html_attributes( $attributes ) . '>';
$html .= $icon;
$html .= '<span>' . sanitize_text_field( $options['text'] ) . '</span>';
$html .= '</' . $html_tag . '>';
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param string $option Optional default is null.
* @return array|mixed
*/
private function get_options( $option = null ) {
return $this->get_items( $this->options, $option );
}
/**
* @param null $option
* @return array
*/
private function get_default_options( $option = null ) {
$default_options = [
'classes' => [ 'e-button' ],
'icon' => '',
'new_tab' => false,
'text' => '',
'type' => '',
'url' => '',
'variant' => '',
'before' => '',
];
if ( null !== $option && -1 !== in_array( $option, $default_options, true ) ) {
return $default_options[ $option ];
}
return $default_options;
}
public function __construct( array $options ) {
$this->options = $this->merge_properties( $this->get_default_options(), $options );
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Elementor\Core\App;
use Elementor\Core\Base\App as BaseApp;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This App class was introduced for backwards compatibility with 3rd parties.
*/
class App extends BaseApp {
const PAGE_ID = 'elementor-app';
public function get_name() {
return 'app-bc';
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Elementor\App\Modules\ImportExport;
use Elementor\App\Modules\ImportExport\Module as Import_Export_Module;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This App class exists for backwards compatibility with 3rd parties.
*
* @deprecated 3.8.0
*/
class Module extends BaseModule {
/**
* @deprecated 3.8.0
*/
const VERSION = '1.0.0';
/**
* @var mixed
* @deprecated 3.8.0
*/
public $import;
/**
* @deprecated 3.8.0
*/
public function get_name() {
return 'import-export-bc';
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Core\App\Modules\KitLibrary\Connect;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library as Kit_Library_Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This App class exists for backwards compatibility with 3rd parties.
*
* @deprecated 3.8.0
*/
class Kit_Library extends Library {
/**
* @deprecated 3.8.0
*/
public function is_connected() {
/** @var Kit_Library_Connect $kit_library */
$kit_library = Plugin::$instance->common->get_component( 'connect' )->get_app( 'kit-library' );
return $kit_library && $kit_library->is_connected();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Core\App\Modules\KitLibrary;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This App class exists for backwards compatibility with 3rd parties.
*
* @deprecated 3.8.0
*/
class Module extends BaseModule {
/**
* @deprecated 3.8.0
*/
const VERSION = '1.0.0';
/**
* @deprecated 3.8.0
*/
public function get_name() {
return 'kit-library-bc';
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Core\App\Modules\Onboarding;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This App class exists for backwards compatibility with 3rd parties.
*
* @deprecated 3.8.0
*/
class Module extends BaseModule {
/**
* @deprecated 3.8.0
*/
const VERSION = '1.0.0';
/**
* @deprecated 3.8.0
*/
public function get_name() {
return 'onboarding-bc';
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Elementor\Core\Base;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Base App
*
* Base app utility class that provides shared functionality of apps.
*
* @since 2.3.0
*/
abstract class App extends Module {
/**
* Print config.
*
* Used to print the app and its components settings as a JavaScript object.
*
* @param string $handle Optional
*
* @since 2.3.0
* @since 2.6.0 added the `$handle` parameter
* @access protected
*/
final protected function print_config( $handle = null ) {
$name = $this->get_name();
$js_var = 'elementor' . str_replace( ' ', '', ucwords( str_replace( '-', ' ', $name ) ) ) . 'Config';
$config = $this->get_settings() + $this->get_components_config();
if ( ! $handle ) {
$handle = 'elementor-' . $name;
}
Utils::print_js_config( $handle, $js_var, $config );
}
/**
* Get components config.
*
* Retrieves the app components settings.
*
* @since 2.3.0
* @access private
*
* @return array
*/
private function get_components_config() {
$settings = [];
foreach ( $this->get_components() as $id => $instance ) {
$settings[ $id ] = $instance->get_settings();
}
return $settings;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Elementor\Core\Base\BackgroundProcess;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Link https://github.com/A5hleyRich/wp-background-processing GPL v2.0
*
* WP Async Request
*
* @package WP-Background-Processing
*/
/**
* Abstract WP_Async_Request class.
*
* @abstract
*/
abstract class WP_Async_Request {
/**
* Prefix
*
* (default value: 'wp')
*
* @var string
* @access protected
*/
protected $prefix = 'wp';
/**
* Action
*
* (default value: 'async_request')
*
* @var string
* @access protected
*/
protected $action = 'async_request';
/**
* Identifier
*
* @var mixed
* @access protected
*/
protected $identifier;
/**
* Data
*
* (default value: [])
*
* @var array
* @access protected
*/
protected $data = [];
/**
* Initiate new async request
*/
public function __construct() {
$this->identifier = $this->prefix . '_' . $this->action;
add_action( 'wp_ajax_' . $this->identifier, [ $this, 'maybe_handle' ] );
add_action( 'wp_ajax_nopriv_' . $this->identifier, [ $this, 'maybe_handle' ] );
}
/**
* Set data used during the request
*
* @param array $data Data.
*
* @return $this
*/
public function data( $data ) {
$this->data = $data;
return $this;
}
/**
* Dispatch the async request
*
* @return array|\WP_Error
*/
public function dispatch() {
$url = add_query_arg( $this->get_query_args(), $this->get_query_url() );
$args = $this->get_post_args();
return wp_remote_post( esc_url_raw( $url ), $args );
}
/**
* Get query args
*
* @return array
*/
protected function get_query_args() {
if ( property_exists( $this, 'query_args' ) ) {
return $this->query_args;
}
return [
'action' => $this->identifier,
'nonce' => wp_create_nonce( $this->identifier ),
];
}
/**
* Get query URL
*
* @return string
*/
protected function get_query_url() {
if ( property_exists( $this, 'query_url' ) ) {
return $this->query_url;
}
return admin_url( 'admin-ajax.php' );
}
/**
* Get post args
*
* @return array
*/
protected function get_post_args() {
if ( property_exists( $this, 'post_args' ) ) {
return $this->post_args;
}
return [
'timeout' => 0.01,
'blocking' => false,
'body' => $this->data,
'cookies' => $_COOKIE,
/** This filter is documented in wp-includes/class-wp-http-streams.php */
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
];
}
/**
* Maybe handle
*
* Check for correct nonce and pass to handler.
*/
public function maybe_handle() {
// Don't lock up other requests while processing
session_write_close();
check_ajax_referer( $this->identifier, 'nonce' );
$this->handle();
wp_die();
}
/**
* Handle
*
* Override this method to perform any actions required
* during the async request.
*/
abstract protected function handle();
}

View File

@@ -0,0 +1,518 @@
<?php
namespace Elementor\Core\Base\BackgroundProcess;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Link https://github.com/A5hleyRich/wp-background-processing GPL v2.0.
*
* WP Background Process
*
* @package WP-Background-Processing
*/
/**
* Abstract WP_Background_Process class.
*
* @abstract
* @extends WP_Async_Request
*/
abstract class WP_Background_Process extends WP_Async_Request {
/**
* Action
*
* (default value: 'background_process')
*
* @var string
* @access protected
*/
protected $action = 'background_process';
/**
* Start time of current process.
*
* (default value: 0)
*
* @var int
* @access protected
*/
protected $start_time = 0;
/**
* Cron_hook_identifier
*
* @var mixed
* @access protected
*/
protected $cron_hook_identifier;
/**
* Cron_interval_identifier
*
* @var mixed
* @access protected
*/
protected $cron_interval_identifier;
/**
* Initiate new background process
*/
public function __construct() {
parent::__construct();
$this->cron_hook_identifier = $this->identifier . '_cron';
$this->cron_interval_identifier = $this->identifier . '_cron_interval';
add_action( $this->cron_hook_identifier, [ $this, 'handle_cron_healthcheck' ] );
add_filter( 'cron_schedules', [ $this, 'schedule_cron_healthcheck' ] );
}
/**
* Dispatch
*
* @access public
* @return array|\WP_Error
*/
public function dispatch() {
// Schedule the cron healthcheck.
$this->schedule_event();
// Perform remote post.
return parent::dispatch();
}
/**
* Push to queue
*
* @param mixed $data Data.
*
* @return $this
*/
public function push_to_queue( $data ) {
$this->data[] = $data;
return $this;
}
/**
* Save queue
*
* @return $this
*/
public function save() {
$key = $this->generate_key();
if ( ! empty( $this->data ) ) {
update_site_option( $key, $this->data );
}
return $this;
}
/**
* Update queue
*
* @param string $key Key.
* @param array $data Data.
*
* @return $this
*/
public function update( $key, $data ) {
if ( ! empty( $data ) ) {
update_site_option( $key, $data );
}
return $this;
}
/**
* Delete queue
*
* @param string $key Key.
*
* @return $this
*/
public function delete( $key ) {
delete_site_option( $key );
return $this;
}
/**
* Generate key
*
* Generates a unique key based on microtime. Queue items are
* given a unique key so that they can be merged upon save.
*
* @param int $length Length.
*
* @return string
*/
protected function generate_key( $length = 64 ) {
$unique = md5( microtime() . wp_rand() );
$prepend = $this->identifier . '_batch_';
return substr( $prepend . $unique, 0, $length );
}
/**
* Maybe process queue
*
* Checks whether data exists within the queue and that
* the process is not already running.
*/
public function maybe_handle() {
// Don't lock up other requests while processing
session_write_close();
if ( $this->is_process_running() ) {
// Background process already running.
wp_die();
}
if ( $this->is_queue_empty() ) {
// No data to process.
wp_die();
}
check_ajax_referer( $this->identifier, 'nonce' );
$this->handle();
wp_die();
}
/**
* Is queue empty
*
* @return bool
*/
protected function is_queue_empty() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
$count = $wpdb->get_var( $wpdb->prepare( "
SELECT COUNT(*)
FROM {$table}
WHERE {$column} LIKE %s
", $key ) );
// phpcs:enable
return ( $count > 0 ) ? false : true;
}
/**
* Is process running
*
* Check whether the current process is already running
* in a background process.
*/
protected function is_process_running() {
if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
// Process already running.
return true;
}
return false;
}
/**
* Lock process
*
* Lock the process so that multiple instances can't run simultaneously.
* Override if applicable, but the duration should be greater than that
* defined in the time_exceeded() method.
*/
protected function lock_process() {
$this->start_time = time(); // Set start time of current process.
$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
}
/**
* Unlock process
*
* Unlock the process so that other instances can spawn.
*
* @return $this
*/
protected function unlock_process() {
delete_site_transient( $this->identifier . '_process_lock' );
return $this;
}
/**
* Get batch
*
* @return \stdClass Return the first batch from the queue
*/
protected function get_batch() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
$key_column = 'option_id';
$value_column = 'option_value';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
$key_column = 'meta_id';
$value_column = 'meta_value';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// Can't use placeholders for table/column names, it will be wrapped by a single quote (') instead of a backquote (`).
$query = $wpdb->get_row( $wpdb->prepare( "
SELECT *
FROM {$table}
WHERE {$column} LIKE %s
ORDER BY {$key_column} ASC
LIMIT 1
", $key ) );
// phpcs:enable
$batch = new \stdClass();
$batch->key = $query->$column;
$batch->data = maybe_unserialize( $query->$value_column );
return $batch;
}
/**
* Handle
*
* Pass each queue item to the task handler, while remaining
* within server memory and time limit constraints.
*/
protected function handle() {
$this->lock_process();
do {
$batch = $this->get_batch();
foreach ( $batch->data as $key => $value ) {
$task = $this->task( $value );
if ( false !== $task ) {
$batch->data[ $key ] = $task;
} else {
unset( $batch->data[ $key ] );
}
if ( $this->time_exceeded() || $this->memory_exceeded() ) {
// Batch limits reached.
break;
}
}
// Update or delete current batch.
if ( ! empty( $batch->data ) ) {
$this->update( $batch->key, $batch->data );
} else {
$this->delete( $batch->key );
}
} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
$this->unlock_process();
// Start next batch or complete process.
if ( ! $this->is_queue_empty() ) {
$this->dispatch();
} else {
$this->complete();
}
wp_die();
}
/**
* Memory exceeded
*
* Ensures the batch process never exceeds 90%
* of the maximum WordPress memory.
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
$current_memory = memory_get_usage( true );
$return = false;
if ( $current_memory >= $memory_limit ) {
$return = true;
}
return apply_filters( $this->identifier . '_memory_exceeded', $return );
}
/**
* Get memory limit
*
* @return int
*/
protected function get_memory_limit() {
if ( function_exists( 'ini_get' ) ) {
$memory_limit = ini_get( 'memory_limit' );
} else {
// Sensible default.
$memory_limit = '128M';
}
if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
// Unlimited, set to 32GB.
$memory_limit = '32000M';
}
return intval( $memory_limit ) * 1024 * 1024;
}
/**
* Time exceeded.
*
* Ensures the batch never exceeds a sensible time limit.
* A timeout limit of 30s is common on shared hosting.
*
* @return bool
*/
protected function time_exceeded() {
$finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
$return = false;
if ( time() >= $finish ) {
$return = true;
}
return apply_filters( $this->identifier . '_time_exceeded', $return );
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
// Unschedule the cron healthcheck.
$this->clear_scheduled_event();
}
/**
* Schedule cron healthcheck
*
* @access public
* @param mixed $schedules Schedules.
* @return mixed
*/
public function schedule_cron_healthcheck( $schedules ) {
$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
if ( property_exists( $this, 'cron_interval' ) ) {
$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
}
// Adds every 5 minutes to the existing schedules.
$schedules[ $this->identifier . '_cron_interval' ] = [
'interval' => MINUTE_IN_SECONDS * $interval,
'display' => sprintf(
/* translators: %d: Interval in minutes. */
esc_html__( 'Every %d minutes', 'elementor' ),
$interval,
),
];
return $schedules;
}
/**
* Handle cron healthcheck
*
* Restart the background process if not already running
* and data exists in the queue.
*/
public function handle_cron_healthcheck() {
if ( $this->is_process_running() ) {
// Background process already running.
exit;
}
if ( $this->is_queue_empty() ) {
// No data to process.
$this->clear_scheduled_event();
exit;
}
$this->handle();
exit;
}
/**
* Schedule event
*/
protected function schedule_event() {
if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
}
}
/**
* Clear scheduled event
*/
protected function clear_scheduled_event() {
$timestamp = wp_next_scheduled( $this->cron_hook_identifier );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
}
}
/**
* Cancel Process
*
* Stop processing queue items, clear cronjob and delete batch.
*/
public function cancel_process() {
if ( ! $this->is_queue_empty() ) {
$batch = $this->get_batch();
$this->delete( $batch->key );
wp_clear_scheduled_hook( $this->cron_hook_identifier );
}
}
/**
* Task
*
* Override this method to perform any actions required on each
* queue item. Return the modified item for further processing
* in the next pass through. Or, return false to remove the
* item from the queue.
*
* @param mixed $item Queue item to iterate over.
*
* @return mixed
*/
abstract protected function task( $item );
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Elementor\Core\Base;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Background_Task_Manager extends BaseModule {
/**
* @var Background_Task
*/
protected $task_runner;
abstract public function get_action();
abstract public function get_plugin_name();
abstract public function get_plugin_label();
abstract public function get_task_runner_class();
abstract public function get_query_limit();
abstract protected function start_run();
public function on_runner_start() {
$logger = Plugin::$instance->logger->get_logger();
$logger->info( $this->get_plugin_name() . '::' . $this->get_action() . ' Started' );
}
public function on_runner_complete( $did_tasks = false ) {
$logger = Plugin::$instance->logger->get_logger();
$logger->info( $this->get_plugin_name() . '::' . $this->get_action() . ' Completed' );
}
public function get_task_runner() {
if ( empty( $this->task_runner ) ) {
$class_name = $this->get_task_runner_class();
$this->task_runner = new $class_name( $this );
}
return $this->task_runner;
}
/**
* @param $flag
* @return void
* // TODO: Replace with a db settings system.
*/
protected function add_flag( $flag ) {
add_option( $this->get_plugin_name() . '_' . $this->get_action() . '_' . $flag, 1 );
}
protected function get_flag( $flag ) {
return get_option( $this->get_plugin_name() . '_' . $this->get_action() . '_' . $flag );
}
protected function delete_flag( $flag ) {
delete_option( $this->get_plugin_name() . '_' . $this->get_action() . '_' . $flag );
}
protected function get_start_action_url() {
return wp_nonce_url( add_query_arg( $this->get_action(), 'run' ), $this->get_action() . 'run' );
}
protected function get_continue_action_url() {
return wp_nonce_url( add_query_arg( $this->get_action(), 'continue' ), $this->get_action() . 'continue' );
}
private function continue_run() {
$runner = $this->get_task_runner();
$runner->continue_run();
}
public function __construct() {
if ( empty( $_GET[ $this->get_action() ] ) ) {
return;
}
Plugin::$instance->init_common();
if ( 'run' === $_GET[ $this->get_action() ] && check_admin_referer( $this->get_action() . 'run' ) ) {
$this->start_run();
}
if ( 'continue' === $_GET[ $this->get_action() ] && check_admin_referer( $this->get_action() . 'continue' ) ) {
$this->continue_run();
}
wp_safe_redirect( remove_query_arg( [ $this->get_action(), '_wpnonce' ] ) );
die;
}
}

View File

@@ -0,0 +1,389 @@
<?php
/**
* Based on https://github.com/woocommerce/woocommerce/blob/master/includes/abstracts/class-wc-background-process.php
* & https://github.com/woocommerce/woocommerce/blob/master/includes/class-wc-background-updater.php
*
* @package Elementor\Core\Base
*/
namespace Elementor\Core\Base;
use Elementor\Plugin;
use Elementor\Core\Base\BackgroundProcess\WP_Background_Process;
defined( 'ABSPATH' ) || exit;
/**
* WC_Background_Process class.
*/
abstract class Background_Task extends WP_Background_Process {
protected $current_item;
/**
* Dispatch updater.
*
* Updater will still run via cron job if this fails for any reason.
*/
public function dispatch() {
$dispatched = parent::dispatch();
if ( is_wp_error( $dispatched ) ) {
wp_die( esc_html( $dispatched ) );
}
}
public function query_col( $sql ) {
global $wpdb;
// Add Calc.
$item = $this->get_current_item();
if ( empty( $item['total'] ) ) {
$sql = preg_replace( '/^SELECT/', 'SELECT SQL_CALC_FOUND_ROWS', $sql );
}
// Add offset & limit.
$sql = preg_replace( '/;$/', '', $sql );
$sql .= ' LIMIT %d, %d;';
$results = $wpdb->get_col( $wpdb->prepare( $sql, $this->get_current_offset(), $this->get_limit() ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( ! empty( $results ) ) {
$this->set_total();
}
return $results;
}
public function should_run_again( $updated_rows ) {
return count( $updated_rows ) === $this->get_limit();
}
public function get_current_offset() {
$limit = $this->get_limit();
return ( $this->current_item['iterate_num'] - 1 ) * $limit;
}
public function get_limit() {
return $this->manager->get_query_limit();
}
public function set_total() {
global $wpdb;
if ( empty( $this->current_item['total'] ) ) {
$total_rows = $wpdb->get_var( 'SELECT FOUND_ROWS();' );
$total_iterates = ceil( $total_rows / $this->get_limit() );
$this->current_item['total'] = $total_iterates;
}
}
/**
* Complete
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$this->manager->on_runner_complete( true );
parent::complete();
}
public function continue_run() {
// Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck().
// This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running.
do_action( $this->cron_hook_identifier );
}
/**
* @return mixed
*/
public function get_current_item() {
return $this->current_item;
}
/**
* Get batch.
*
* @return \stdClass Return the first batch from the queue.
*/
protected function get_batch() {
$batch = parent::get_batch();
$batch->data = array_filter( (array) $batch->data );
return $batch;
}
/**
* Handle cron healthcheck
*
* Restart the background process if not already running
* and data exists in the queue.
*/
public function handle_cron_healthcheck() {
if ( $this->is_process_running() ) {
// Background process already running.
return;
}
if ( $this->is_queue_empty() ) {
// No data to process.
$this->clear_scheduled_event();
return;
}
$this->handle();
}
/**
* Schedule fallback event.
*/
protected function schedule_event() {
if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier );
}
}
/**
* Is the updater running?
*
* @return boolean
*/
public function is_running() {
return false === $this->is_queue_empty();
}
/**
* See if the batch limit has been exceeded.
*
* @return bool
*/
protected function batch_limit_exceeded() {
return $this->time_exceeded() || $this->memory_exceeded();
}
/**
* Handle.
*
* Pass each queue item to the task handler, while remaining
* within server memory and time limit constraints.
*/
protected function handle() {
$this->manager->on_runner_start();
$this->lock_process();
do {
$batch = $this->get_batch();
foreach ( $batch->data as $key => $value ) {
$task = $this->task( $value );
if ( false !== $task ) {
$batch->data[ $key ] = $task;
} else {
unset( $batch->data[ $key ] );
}
if ( $this->batch_limit_exceeded() ) {
// Batch limits reached.
break;
}
}
// Update or delete current batch.
if ( ! empty( $batch->data ) ) {
$this->update( $batch->key, $batch->data );
} else {
$this->delete( $batch->key );
}
} while ( ! $this->batch_limit_exceeded() && ! $this->is_queue_empty() );
$this->unlock_process();
// Start next batch or complete process.
if ( ! $this->is_queue_empty() ) {
$this->dispatch();
} else {
$this->complete();
}
}
/**
* Use the protected `is_process_running` method as a public method.
*
* @return bool
*/
public function is_process_locked() {
return $this->is_process_running();
}
public function handle_immediately( $callbacks ) {
$this->manager->on_runner_start();
$this->lock_process();
foreach ( $callbacks as $callback ) {
$item = [
'callback' => $callback,
];
do {
$item = $this->task( $item );
} while ( $item );
}
$this->unlock_process();
}
/**
* Task
*
* Override this method to perform any actions required on each
* queue item. Return the modified item for further processing
* in the next pass through. Or, return false to remove the
* item from the queue.
*
* @param array $item
*
* @return array|bool
*/
protected function task( $item ) {
$result = false;
if ( ! isset( $item['iterate_num'] ) ) {
$item['iterate_num'] = 1;
}
$logger = Plugin::$instance->logger->get_logger();
$callback = $this->format_callback_log( $item );
if ( is_callable( $item['callback'] ) ) {
$progress = '';
if ( 1 < $item['iterate_num'] ) {
if ( empty( $item['total'] ) ) {
$progress = sprintf( '(x%s)', $item['iterate_num'] );
} else {
$percent = ceil( $item['iterate_num'] / ( $item['total'] / 100 ) );
$progress = sprintf( '(%s of %s, %s%%)', $item['iterate_num'], $item['total'], $percent );
}
}
$logger->info( sprintf( '%s Start %s', $callback, $progress ) );
$this->current_item = $item;
$result = (bool) call_user_func( $item['callback'], $this );
// get back the updated item.
$item = $this->current_item;
$this->current_item = null;
if ( $result ) {
if ( empty( $item['total'] ) ) {
$logger->info( sprintf( '%s callback needs to run again', $callback ) );
} elseif ( 1 === $item['iterate_num'] ) {
$logger->info( sprintf( '%s callback needs to run more %d times', $callback, $item['total'] - $item['iterate_num'] ) );
}
++$item['iterate_num'];
} else {
$logger->info( sprintf( '%s Finished', $callback ) );
}
} else {
$logger->notice( sprintf( 'Could not find %s callback', $callback ) );
}
return $result ? $item : false;
}
/**
* Schedule cron healthcheck.
*
* @param array $schedules Schedules.
* @return array
*/
public function schedule_cron_healthcheck( $schedules ) {
$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
// Adds every 5 minutes to the existing schedules.
$schedules[ $this->identifier . '_cron_interval' ] = [
'interval' => MINUTE_IN_SECONDS * $interval,
'display' => sprintf(
/* translators: %d: Interval in minutes. */
esc_html__( 'Every %d minutes', 'elementor' ),
$interval
),
];
return $schedules;
}
/**
* See if the batch limit has been exceeded.
*
* @return bool
*/
public function is_memory_exceeded() {
return $this->memory_exceeded();
}
/**
* Delete all batches.
*
* @return self
*/
public function delete_all_batches() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine.
return $this;
}
/**
* Kill process.
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public function kill_process() {
if ( ! $this->is_queue_empty() ) {
$this->delete_all_batches();
wp_clear_scheduled_hook( $this->cron_hook_identifier );
}
}
public function set_current_item( $item ) {
$this->current_item = $item;
}
protected function format_callback_log( $item ) {
return implode( '::', (array) $item['callback'] );
}
/**
* @var \Elementor\Core\Base\Background_Task_Manager
*/
protected $manager;
public function __construct( $manager ) {
$this->manager = $manager;
// Uses unique prefix per blog so each blog has separate queue.
$this->prefix = 'elementor_' . get_current_blog_id();
$this->action = $this->manager->get_action();
parent::__construct();
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace Elementor\Core\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Base Object
*
* Base class that provides basic settings handling functionality.
*
* @since 2.3.0
*/
class Base_Object {
/**
* Settings.
*
* Holds the object settings.
*
* @access private
*
* @var array
*/
private $settings;
/**
* Get Settings.
*
* @since 2.3.0
* @access public
*
* @param string $setting Optional. The key of the requested setting. Default is null.
*
* @return mixed An array of all settings, or a single value if `$setting` was specified.
*/
final public function get_settings( $setting = null ) {
$this->ensure_settings();
return self::get_items( $this->settings, $setting );
}
/**
* Set settings.
*
* @since 2.3.0
* @access public
*
* @param array|string $key If key is an array, the settings are overwritten by that array. Otherwise, the
* settings of the key will be set to the given `$value` param.
*
* @param mixed $value Optional. Default is null.
*/
final public function set_settings( $key, $value = null ) {
$this->ensure_settings();
if ( is_array( $key ) ) {
$this->settings = $key;
} else {
$this->settings[ $key ] = $value;
}
}
/**
* Delete setting.
*
* Deletes the settings array or a specific key of the settings array if `$key` is specified.
*
* @since 2.3.0
* @access public
*
* @param string $key Optional. Default is null.
*/
public function delete_setting( $key = null ) {
if ( $key ) {
unset( $this->settings[ $key ] );
} else {
$this->settings = [];
}
}
final public function merge_properties( array $default_props, array $custom_props, array $allowed_props_keys = [] ) {
$props = array_replace_recursive( $default_props, $custom_props );
if ( $allowed_props_keys ) {
$props = array_intersect_key( $props, array_flip( $allowed_props_keys ) );
}
return $props;
}
/**
* Get items.
*
* Utility method that receives an array with a needle and returns all the
* items that match the needle. If needle is not defined the entire haystack
* will be returned.
*
* @since 2.3.0
* @access protected
* @static
*
* @param array $haystack An array of items.
* @param string $needle Optional. Needle. Default is null.
*
* @return mixed The whole haystack or the needle from the haystack when requested.
*/
final protected static function get_items( array $haystack, $needle = null ) {
if ( $needle ) {
return isset( $haystack[ $needle ] ) ? $haystack[ $needle ] : null;
}
return $haystack;
}
/**
* Get init settings.
*
* Used to define the default/initial settings of the object. Inheriting classes may implement this method to define
* their own default/initial settings.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
return [];
}
/**
* Ensure settings.
*
* Ensures that the `$settings` member is initialized
*
* @since 2.3.0
* @access private
*/
private function ensure_settings() {
if ( null === $this->settings ) {
$this->settings = $this->get_init_settings();
}
}
/**
* Has Own Method
*
* Used for check whether the method passed as a parameter was declared in the current instance or inherited.
* If a base_class_name is passed, it checks whether the method was declared in that class. If the method's
* declaring class is the class passed as $base_class_name, it returns false. Otherwise (method was NOT declared
* in $base_class_name), it returns true.
*
* Example #1 - only $method_name is passed:
* The initial declaration of `register_controls()` happens in the `Controls_Stack` class. However, all
* widgets which have their own controls declare this function as well, overriding the original
* declaration. If `has_own_method()` would be called by a Widget's class which implements `register_controls()`,
* with 'register_controls' passed as the first parameter - `has_own_method()` will return true. If the Widget
* does not declare `register_controls()`, `has_own_method()` will return false.
*
* Example #2 - both $method_name and $base_class_name are passed
* In this example, the widget class inherits from a base class `Widget_Base`, and the base implements
* `register_controls()` to add certain controls to all widgets inheriting from it. `has_own_method()` is called by
* the widget, with the string 'register_controls' passed as the first parameter, and 'Elementor\Widget_Base' (its full name
* including the namespace) passed as the second parameter. If the widget class implements `register_controls()`,
* `has_own_method` will return true. If the widget class DOESN'T implement `register_controls()`, it will return
* false (because `Widget_Base` is the declaring class for `register_controls()`, and not the class that called
* `has_own_method()`).
*
* @since 3.1.0
*
* @param string $method_name
* @param string $base_class_name
*
* @return bool True if the method was declared by the current instance, False if it was inherited.
*/
public function has_own_method( $method_name, $base_class_name = null ) {
try {
$reflection_method = new \ReflectionMethod( $this, $method_name );
// If a ReflectionMethod is successfully created, get its declaring class.
$declaring_class = $reflection_method->getDeclaringClass();
} catch ( \Exception $e ) {
return false;
}
if ( $base_class_name ) {
return $base_class_name !== $declaring_class->name;
}
return get_called_class() === $declaring_class->name;
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace Elementor\Core\Base;
use Elementor\Core\Admin\Admin_Notices;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class DB_Upgrades_Manager extends Background_Task_Manager {
protected $current_version = null;
protected $query_limit = 100;
abstract public function get_new_version();
abstract public function get_version_option_name();
abstract public function get_upgrades_class();
abstract public function get_updater_label();
public function get_task_runner_class() {
return 'Elementor\Core\Upgrade\Updater';
}
public function get_query_limit() {
return $this->query_limit;
}
public function set_query_limit( $limit ) {
$this->query_limit = $limit;
}
public function get_current_version() {
if ( null === $this->current_version ) {
$this->current_version = get_option( $this->get_version_option_name() );
}
return $this->current_version;
}
public function should_upgrade() {
$current_version = $this->get_current_version();
// It's a new install.
if ( ! $current_version ) {
$this->update_db_version();
return false;
}
return version_compare( $this->get_new_version(), $current_version, '>' );
}
public function on_runner_start() {
parent::on_runner_start();
if ( ! defined( 'IS_ELEMENTOR_UPGRADE' ) ) {
define( 'IS_ELEMENTOR_UPGRADE', true );
}
}
public function on_runner_complete( $did_tasks = false ) {
$logger = Plugin::$instance->logger->get_logger();
$logger->info( 'Elementor data updater process has been completed.', [
'meta' => [
'plugin' => $this->get_plugin_label(),
'from' => $this->current_version,
'to' => $this->get_new_version(),
],
] );
$this->clear_cache();
$this->update_db_version();
if ( $did_tasks ) {
$this->add_flag( 'completed' );
}
}
protected function clear_cache() {
Plugin::$instance->files_manager->clear_cache();
}
public function admin_notice_start_upgrade() {
/**
* @var Admin_Notices $admin_notices
*/
$admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
$options = [
'title' => $this->get_updater_label(),
'description' => esc_html__( 'Your site database needs to be updated to the latest version.', 'elementor' ),
'type' => 'error',
'icon' => false,
'button' => [
'text' => esc_html__( 'Update Now', 'elementor' ),
'url' => $this->get_start_action_url(),
'class' => 'e-button e-button--cta',
],
];
$admin_notices->print_admin_notice( $options );
}
public function admin_notice_upgrade_is_running() {
/**
* @var Admin_Notices $admin_notices
*/
$admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
$options = [
'title' => $this->get_updater_label(),
'description' => esc_html__( 'Database update process is running in the background. Taking a while?', 'elementor' ),
'type' => 'warning',
'icon' => false,
'button' => [
'text' => esc_html__( 'Click here to run it now', 'elementor' ),
'url' => $this->get_continue_action_url(),
'class' => 'e-button e-button--primary',
],
];
$admin_notices->print_admin_notice( $options );
}
public function admin_notice_upgrade_is_completed() {
$this->delete_flag( 'completed' );
$message = esc_html__( 'The database update process is now complete. Thank you for updating to the latest version!', 'elementor' );
/**
* @var Admin_Notices $admin_notices
*/
$admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
$options = [
'description' => '<b>' . $this->get_updater_label() . '</b> - ' . $message,
'type' => 'success',
'icon' => false,
];
$admin_notices->print_admin_notice( $options );
}
/**
* @access protected
*/
protected function start_run() {
$updater = $this->get_task_runner();
if ( $updater->is_running() ) {
return;
}
$upgrade_callbacks = $this->get_upgrade_callbacks();
if ( empty( $upgrade_callbacks ) ) {
$this->on_runner_complete();
return;
}
$this->clear_cache();
foreach ( $upgrade_callbacks as $callback ) {
$updater->push_to_queue( [
'callback' => $callback,
] );
}
$updater->save()->dispatch();
Plugin::$instance->logger->get_logger()->info( 'Elementor data updater process has been queued.', [
'meta' => [
'plugin' => $this->get_plugin_label(),
'from' => $this->current_version,
'to' => $this->get_new_version(),
],
] );
}
protected function update_db_version() {
update_option( $this->get_version_option_name(), $this->get_new_version() );
}
public function get_upgrade_callbacks() {
$prefix = '_v_';
$upgrades_class = $this->get_upgrades_class();
$upgrades_reflection = new \ReflectionClass( $upgrades_class );
$callbacks = [];
foreach ( $upgrades_reflection->getMethods() as $method ) {
$method_name = $method->getName();
if ( '_on_each_version' === $method_name ) {
$callbacks[] = [ $upgrades_class, $method_name ];
continue;
}
if ( false === strpos( $method_name, $prefix ) ) {
continue;
}
if ( ! preg_match_all( "/$prefix(\d+_\d+_\d+)/", $method_name, $matches ) ) {
continue;
}
$method_version = str_replace( '_', '.', $matches[1][0] );
if ( ! version_compare( $method_version, $this->current_version, '>' ) ) {
continue;
}
$callbacks[] = [ $upgrades_class, $method_name ];
}
return $callbacks;
}
public function __construct() {
// If upgrade is completed - show the notice only for admins.
// Note: in this case `should_upgrade` returns false, because it's already upgraded.
if ( is_admin() && current_user_can( 'update_plugins' ) && $this->get_flag( 'completed' ) ) {
add_action( 'admin_notices', [ $this, 'admin_notice_upgrade_is_completed' ] );
}
if ( ! $this->should_upgrade() ) {
return;
}
$updater = $this->get_task_runner();
$this->start_run();
if ( $updater->is_running() && current_user_can( 'update_plugins' ) ) {
add_action( 'admin_notices', [ $this, 'admin_notice_upgrade_is_running' ] );
}
parent::__construct();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,204 @@
<?php
namespace Elementor\Core\Base\Elements_Iteration_Actions;
use Elementor\Conditions;
use Elementor\Element_Base;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Assets extends Base {
const ASSETS_META_KEY = '_elementor_page_assets';
/**
* Default value must be empty.
*
* @var array
*/
private $page_assets;
/**
* Default value must be empty.
*
* @var array
*/
private $saved_page_assets;
public function element_action( Element_Base $element_data ) {
$settings = $element_data->get_active_settings();
$controls = $element_data->get_controls();
$element_assets = $this->get_assets( $settings, $controls );
$element_assets_depend = [
'styles' => $element_data->get_style_depends(),
'scripts' => array_merge( $element_data->get_script_depends(), $element_data->get_global_scripts() ),
];
if ( $element_assets_depend ) {
foreach ( $element_assets_depend as $assets_type => $assets ) {
if ( empty( $assets ) ) {
continue;
}
if ( ! isset( $element_assets[ $assets_type ] ) ) {
$element_assets[ $assets_type ] = [];
}
foreach ( $assets as $asset_name ) {
if ( ! in_array( $asset_name, $element_assets[ $assets_type ], true ) ) {
$element_assets[ $assets_type ][] = $asset_name;
}
}
}
}
if ( $element_assets ) {
$this->update_page_assets( $element_assets );
}
}
public function is_action_needed() {
// No need to evaluate in preview mode, will be made in the saving process.
if ( Plugin::$instance->preview->is_preview_mode() ) {
return false;
}
$page_assets = $this->get_saved_page_assets();
// When $page_assets is array it means that the assets registration has already been made at least once.
if ( is_array( $page_assets ) ) {
return false;
}
return true;
}
public function after_elements_iteration() {
// In case that the page assets value is empty, it should still be saved as an empty array as an indication that at lease one iteration has occurred.
if ( ! is_array( $this->page_assets ) ) {
$this->page_assets = [];
}
$this->get_document_assets();
// Saving the page assets data.
$this->document->update_meta( self::ASSETS_META_KEY, $this->page_assets );
if ( 'render' === $this->mode && $this->page_assets ) {
Plugin::$instance->assets_loader->enable_assets( $this->page_assets );
}
}
private function get_saved_page_assets( $force_meta_fetch = false ) {
if ( ! is_array( $this->saved_page_assets ) || $force_meta_fetch ) {
$this->saved_page_assets = $this->document->get_meta( self::ASSETS_META_KEY );
}
return $this->saved_page_assets;
}
private function update_page_assets( $new_assets ) {
if ( ! is_array( $this->page_assets ) ) {
$this->page_assets = [];
}
foreach ( $new_assets as $assets_type => $assets_type_data ) {
if ( ! isset( $this->page_assets[ $assets_type ] ) ) {
$this->page_assets[ $assets_type ] = [];
}
foreach ( $assets_type_data as $asset_name ) {
if ( ! in_array( $asset_name, $this->page_assets[ $assets_type ], true ) ) {
$this->page_assets[ $assets_type ][] = $asset_name;
}
}
}
}
private function get_assets( $settings, $controls ) {
$assets = [];
foreach ( $settings as $setting_key => $setting ) {
if ( ! isset( $controls[ $setting_key ] ) ) {
continue;
}
$control = $controls[ $setting_key ];
// Enabling assets loading from the registered control fields.
if ( ! empty( $control['assets'] ) ) {
foreach ( $control['assets'] as $assets_type => $dependencies ) {
foreach ( $dependencies as $dependency ) {
if ( ! empty( $dependency['conditions'] ) ) {
$is_condition_fulfilled = Conditions::check( $dependency['conditions'], $settings );
if ( ! $is_condition_fulfilled ) {
continue;
}
}
if ( ! isset( $assets[ $assets_type ] ) ) {
$assets[ $assets_type ] = [];
}
$assets[ $assets_type ][] = $dependency['name'];
}
}
}
// Enabling assets loading from the control object.
$control_obj = Plugin::$instance->controls_manager->get_control( $control['type'] );
$control_conditional_assets = $control_obj::get_assets( $setting );
if ( $control_conditional_assets ) {
foreach ( $control_conditional_assets as $assets_type => $dependencies ) {
foreach ( $dependencies as $dependency ) {
if ( ! isset( $assets[ $assets_type ] ) ) {
$assets[ $assets_type ] = [];
}
$assets[ $assets_type ][] = $dependency;
}
}
}
}
return $assets;
}
private function get_document_assets() {
$document_id = $this->document->get_post()->ID;
// Getting the document instance in order to get the most updated settings.
$updated_document = Plugin::$instance->documents->get( $document_id, false );
$document_settings = $updated_document->get_settings();
$document_controls = $this->document->get_controls();
$document_assets = $this->get_assets( $document_settings, $document_controls );
if ( $document_assets ) {
$this->update_page_assets( $document_assets );
}
}
public function __construct( $document ) {
parent::__construct( $document );
// No need to enable assets in preview mode, all assets will be loaded by default by the assets loader.
if ( Plugin::$instance->preview->is_preview_mode() ) {
return;
}
$page_assets = $this->get_saved_page_assets();
// If $page_assets is not empty then enabling the assets for loading.
if ( $page_assets ) {
Plugin::$instance->assets_loader->enable_assets( $page_assets );
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\Core\Base\Elements_Iteration_Actions;
use Elementor\Element_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base {
/**
* The current document that the Base class instance was created from.
*
* @var \Elementor\Core\Document
*/
protected $document;
/**
* Indicates if the methods are being triggered on page save or at render time (value will be either 'save' or 'render').
*
* @var string
*/
protected $mode = '';
/**
* Is Action Needed.
*
* Runs only at runtime and used as a flag to determine if all methods should run on page render.
* If returns false, all methods will run only on page save.
* If returns true, all methods will run on both page render and on save.
*
* @since 3.3.0
* @access public
*
* @return bool
*/
abstract public function is_action_needed();
/**
* Unique Element Action.
*
* Will be triggered for each unique page element - section / column / widget unique type (heading, icon etc.).
*
* @since 3.3.0
* @access public
*
* @return void
*/
public function unique_element_action( Element_Base $element_data ) {}
/**
* Element Action.
*
* Will be triggered for each page element - section / column / widget.
*
* @since 3.3.0
* @access public
*
* @return void
*/
public function element_action( Element_Base $element_data ) {}
/**
* After Elements Iteration.
*
* Will be triggered after all page elements iteration has ended.
*
* @since 3.3.0
* @access public
*
* @return void
*/
public function after_elements_iteration() {}
public function set_mode( $mode ) {
$this->mode = $mode;
}
public function __construct( $document ) {
$this->document = $document;
}
}

View File

@@ -0,0 +1,358 @@
<?php
namespace Elementor\Core\Base;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor module.
*
* An abstract class that provides the needed properties and methods to
* manage and handle modules in inheriting classes.
*
* @since 1.7.0
* @abstract
*/
abstract class Module extends Base_Object {
/**
* Module class reflection.
*
* Holds the information about a class.
*
* @since 1.7.0
* @access private
*
* @var \ReflectionClass
*/
private $reflection;
/**
* Module components.
*
* Holds the module components.
*
* @since 1.7.0
* @access private
*
* @var array
*/
private $components = [];
/**
* Module instance.
*
* Holds the module instance.
*
* @since 1.7.0
* @access protected
*
* @var Module
*/
protected static $_instances = [];
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 1.7.0
* @access public
* @abstract
*
* @return string Module name.
*/
abstract public function get_name();
/**
* Instance.
*
* Ensures only one instance of the module class is loaded or can be loaded.
*
* @since 1.7.0
* @access public
* @static
*
* @return $this An instance of the class.
*/
public static function instance() {
$class_name = static::class_name();
if ( empty( static::$_instances[ $class_name ] ) ) {
static::$_instances[ $class_name ] = new static();
}
return static::$_instances[ $class_name ];
}
/**
* @since 2.0.0
* @access public
* @static
*/
public static function is_active() {
return true;
}
/**
* Class name.
*
* Retrieve the name of the class.
*
* @since 1.7.0
* @access public
* @static
*/
public static function class_name() {
return get_called_class();
}
public static function get_experimental_data() {
return [];
}
/**
* Clone.
*
* Disable class cloning and throw an error on object clone.
*
* The whole idea of the singleton design pattern is that there is a single
* object. Therefore, we don't want the object to be cloned.
*
* @since 1.7.0
* @access public
*/
public function __clone() {
_doing_it_wrong(
__FUNCTION__,
sprintf( 'Cloning instances of the singleton "%s" class is forbidden.', get_class( $this ) ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'1.0.0'
);
}
/**
* Wakeup.
*
* Disable unserializing of the class.
*
* @since 1.7.0
* @access public
*/
public function __wakeup() {
_doing_it_wrong(
__FUNCTION__,
sprintf( 'Unserializing instances of the singleton "%s" class is forbidden.', get_class( $this ) ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'1.0.0'
);
}
/**
* @since 2.0.0
* @access public
*/
public function get_reflection() {
if ( null === $this->reflection ) {
$this->reflection = new \ReflectionClass( $this );
}
return $this->reflection;
}
/**
* Add module component.
*
* Add new component to the current module.
*
* @since 1.7.0
* @access public
*
* @param string $id Component ID.
* @param mixed $instance An instance of the component.
*/
public function add_component( $id, $instance ) {
$this->components[ $id ] = $instance;
}
/**
* @since 2.3.0
* @access public
* @return Module[]
*/
public function get_components() {
return $this->components;
}
/**
* Get module component.
*
* Retrieve the module component.
*
* @since 1.7.0
* @access public
*
* @param string $id Component ID.
*
* @return mixed An instance of the component, or `false` if the component
* doesn't exist.
*/
public function get_component( $id ) {
if ( isset( $this->components[ $id ] ) ) {
return $this->components[ $id ];
}
return false;
}
/**
* Get assets url.
*
* @since 2.3.0
* @access protected
*
* @param string $file_name
* @param string $file_extension
* @param string $relative_url Optional. Default is null.
* @param string $add_min_suffix Optional. Default is 'default'.
*
* @return string
*/
final protected function get_assets_url( $file_name, $file_extension, $relative_url = null, $add_min_suffix = 'default' ) {
static $is_test_mode = null;
if ( null === $is_test_mode ) {
$is_test_mode = Utils::is_script_debug() || Utils::is_elementor_tests();
}
if ( ! $relative_url ) {
$relative_url = $this->get_assets_relative_url() . $file_extension . '/';
}
$url = $this->get_assets_base_url() . $relative_url . $file_name;
if ( 'default' === $add_min_suffix ) {
$add_min_suffix = ! $is_test_mode;
}
if ( $add_min_suffix ) {
$url .= '.min';
}
return $url . '.' . $file_extension;
}
/**
* Get js assets url
*
* @since 2.3.0
* @access protected
*
* @param string $file_name
* @param string $relative_url Optional. Default is null.
* @param string $add_min_suffix Optional. Default is 'default'.
*
* @return string
*/
final protected function get_js_assets_url( $file_name, $relative_url = null, $add_min_suffix = 'default' ) {
return $this->get_assets_url( $file_name, 'js', $relative_url, $add_min_suffix );
}
/**
* Get css assets url
*
* @since 2.3.0
* @access protected
*
* @param string $file_name
* @param string $relative_url Optional. Default is null.
* @param string $add_min_suffix Optional. Default is 'default'.
* @param bool $add_direction_suffix Optional. Default is `false`.
*
* @return string
*/
final protected function get_css_assets_url( $file_name, $relative_url = null, $add_min_suffix = 'default', $add_direction_suffix = false ) {
static $direction_suffix = null;
if ( ! $direction_suffix ) {
$direction_suffix = is_rtl() ? '-rtl' : '';
}
if ( $add_direction_suffix ) {
$file_name .= $direction_suffix;
}
return $this->get_assets_url( $file_name, 'css', $relative_url, $add_min_suffix );
}
/**
* Get Frontend File URL
*
* Returns the URL for the CSS file to be loaded in the front end. If requested via the second parameter, a custom
* file is generated based on a passed template file name. Otherwise, the URL for the default CSS file is returned.
*
* @since 3.24.0
*
* @access public
*
* @param string $file_name
* @param boolean $has_custom_breakpoints
*
* @return string frontend file URL
*/
public function get_frontend_file_url( $file_name, $has_custom_breakpoints ) {
return Plugin::$instance->frontend->get_frontend_file_url( $file_name, $has_custom_breakpoints );
}
/**
* Get assets base url
*
* @since 2.6.0
* @access protected
*
* @return string
*/
protected function get_assets_base_url() {
return ELEMENTOR_URL;
}
/**
* Get assets relative url
*
* @since 2.3.0
* @access protected
*
* @return string
*/
protected function get_assets_relative_url() {
return 'assets/';
}
/**
* Get the module's associated widgets.
*
* @return string[]
*/
protected function get_widgets() {
return [];
}
/**
* Initialize the module related widgets.
*/
public function init_widgets() {
$widget_manager = Plugin::instance()->widgets_manager;
foreach ( $this->get_widgets() as $widget ) {
$class_name = $this->get_reflection()->getNamespaceName() . '\Widgets\\' . $widget;
$widget_manager->register( new $class_name() );
}
}
public function __construct() {
add_action( 'elementor/widgets/register', [ $this, 'init_widgets' ] );
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace Elementor\Core\Base\Providers;
class Social_Network_Provider {
private static array $social_networks = [];
public const FACEBOOK = 'Facebook';
public const TWITTER = 'X (Twitter)';
public const INSTAGRAM = 'Instagram';
public const LINKEDIN = 'LinkedIn';
public const PINTEREST = 'Pinterest';
public const YOUTUBE = 'YouTube';
public const TIKTOK = 'TikTok';
public const WHATSAPP = 'WhatsApp';
public const APPLEMUSIC = 'Apple Music';
public const SPOTIFY = 'Spotify';
public const SOUNDCLOUD = 'SoundCloud';
public const BEHANCE = 'Behance';
public const DRIBBBLE = 'Dribbble';
public const VIMEO = 'Vimeo';
public const WAZE = 'Waze';
public const MESSENGER = 'Messenger';
public const TELEPHONE = 'Telephone';
public const EMAIL = 'Email';
public const URL = 'Url';
public const FILE_DOWNLOAD = 'File Download';
public const SMS = 'SMS';
public const VIBER = 'VIBER';
public const SKYPE = 'Skype';
public const VCF = 'Save contact (vCard)';
public static function get_social_networks_icons(): array {
static::init_social_networks_array_if_empty();
static $icons = [];
if ( empty( $icons ) ) {
foreach ( static::$social_networks as $network => $data ) {
$icons[ $network ] = $data['icon'];
}
}
return $icons;
}
public static function get_icon_mapping( string $platform ): string {
static::init_social_networks_array_if_empty();
if ( isset( self::$social_networks[ $platform ]['icon'] ) ) {
return self::$social_networks[ $platform ]['icon'];
}
return '';
}
public static function get_name_mapping( string $platform ): string {
static::init_social_networks_array_if_empty();
if ( isset( self::$social_networks[ $platform ]['name'] ) ) {
return self::$social_networks[ $platform ]['name'];
}
return '';
}
public static function get_text_mapping( string $platform ): string {
static::init_social_networks_array_if_empty();
if ( isset( self::$social_networks[ $platform ]['text'] ) ) {
return self::$social_networks[ $platform ]['text'];
}
return '';
}
public static function get_social_networks_text( $providers = [] ): array {
static::init_social_networks_array_if_empty();
static $texts = [];
if ( empty( $texts ) ) {
foreach ( static::$social_networks as $network => $data ) {
$texts[ $network ] = $data['text'];
}
}
if ( $providers ) {
return array_intersect_key( $texts, array_flip( $providers ) );
}
return $texts;
}
private static function init_social_networks_array_if_empty(): void {
if ( ! empty( static::$social_networks ) ) {
return;
}
static::$social_networks[ static::VCF ] = [
'text' => esc_html__( 'Save contact (vCard)', 'elementor' ),
'icon' => 'fab fa-outlook',
'name' => 'vcf',
];
static::$social_networks[ static::FACEBOOK ] = [
'text' => esc_html__( 'Facebook', 'elementor' ),
'icon' => 'fab fa-facebook',
'name' => 'facebook',
];
static::$social_networks[ static::TWITTER ] = [
'text' => esc_html__( 'X (Twitter)', 'elementor' ),
'icon' => 'fab fa-x-twitter',
'name' => 'x-twitter',
];
static::$social_networks[ static::INSTAGRAM ] = [
'text' => esc_html__( 'Instagram', 'elementor' ),
'icon' => 'fab fa-instagram',
'name' => 'instagram',
];
static::$social_networks[ static::LINKEDIN ] = [
'text' => esc_html__( 'LinkedIn', 'elementor' ),
'icon' => 'fab fa-linkedin-in',
'name' => 'linkedin',
];
static::$social_networks[ static::PINTEREST ] = [
'text' => esc_html__( 'Pinterest', 'elementor' ),
'icon' => 'fab fa-pinterest',
'name' => 'pinterest',
];
static::$social_networks[ static::YOUTUBE ] = [
'text' => esc_html__( 'YouTube', 'elementor' ),
'icon' => 'fab fa-youtube',
'name' => 'youtube',
];
static::$social_networks[ static::TIKTOK ] = [
'text' => esc_html__( 'TikTok', 'elementor' ),
'icon' => 'fab fa-tiktok',
'name' => 'tiktok',
];
static::$social_networks[ static::WHATSAPP ] = [
'text' => esc_html__( 'WhatsApp', 'elementor' ),
'icon' => 'fab fa-whatsapp',
'name' => 'whatsapp',
];
static::$social_networks[ static::APPLEMUSIC ] = [
'text' => esc_html__( 'Apple Music', 'elementor' ),
'icon' => 'fa fa-music',
'name' => 'apple-music',
];
static::$social_networks[ static::SPOTIFY ] = [
'text' => esc_html__( 'Spotify', 'elementor' ),
'icon' => 'fab fa-spotify',
'name' => 'spotify',
];
static::$social_networks[ static::SOUNDCLOUD ] = [
'text' => esc_html__( 'SoundCloud', 'elementor' ),
'icon' => 'fab fa-soundcloud',
'name' => 'soundcloud',
];
static::$social_networks[ static::BEHANCE ] = [
'text' => esc_html__( 'Behance', 'elementor' ),
'icon' => 'fab fa-behance',
'name' => 'behance',
];
static::$social_networks[ static::DRIBBBLE ] = [
'text' => esc_html__( 'Dribbble', 'elementor' ),
'icon' => 'fab fa-dribbble',
'name' => 'dribble',
];
static::$social_networks[ static::VIMEO ] = [
'text' => esc_html__( 'Vimeo', 'elementor' ),
'icon' => 'fab fa-vimeo-v',
'name' => 'vimeo',
];
static::$social_networks[ static::WAZE ] = [
'text' => esc_html__( 'Waze', 'elementor' ),
'icon' => 'fab fa-waze',
'name' => 'waze',
];
static::$social_networks[ static::MESSENGER ] = [
'text' => esc_html__( 'Messenger', 'elementor' ),
'icon' => 'fab fa-facebook-messenger',
'name' => 'messenger',
];
static::$social_networks[ static::TELEPHONE ] = [
'text' => esc_html__( 'Telephone', 'elementor' ),
'icon' => 'fas fa-phone-alt',
'name' => 'phone',
];
static::$social_networks[ static::EMAIL ] = [
'text' => esc_html__( 'Email', 'elementor' ),
'icon' => 'fas fa-envelope',
'name' => 'email',
];
static::$social_networks[ static::URL ] = [
'text' => esc_html__( 'URL', 'elementor' ),
'icon' => 'fas fa-globe',
'name' => 'url',
];
static::$social_networks[ static::FILE_DOWNLOAD ] = [
'text' => esc_html__( 'File Download', 'elementor' ),
'icon' => 'fas fa-download',
'name' => 'download',
];
static::$social_networks[ static::SMS ] = [
'text' => esc_html__( 'SMS', 'elementor' ),
'icon' => 'fas fa-sms',
'name' => 'sms',
];
static::$social_networks[ static::VIBER ] = [
'text' => esc_html__( 'Viber', 'elementor' ),
'icon' => 'fab fa-viber',
'name' => 'viber',
];
static::$social_networks[ static::SKYPE ] = [
'text' => esc_html__( 'Skype', 'elementor' ),
'icon' => 'fab fa-skype',
'name' => 'skype',
];
}
public static function build_messenger_link( string $username ) {
return 'https://m.me/' . $username;
}
public static function build_email_link( array $data, string $prefix ) {
$email = $data[ $prefix . '_mail' ] ?? '';
$subject = $data[ $prefix . '_mail_subject' ] ?? '';
$body = $data[ $prefix . '_mail_body' ] ?? '';
if ( ! $email ) {
return '';
}
$link = 'mailto:' . $email;
if ( $subject ) {
$link .= '?subject=' . $subject;
}
if ( $body ) {
$link .= $subject ? '&' : '?';
$link .= 'body=' . $body;
}
return $link;
}
public static function build_viber_link( string $action, string $number ) {
if ( empty( $number ) ) {
return '';
}
return add_query_arg( [
'number' => urlencode( $number ),
], 'viber://' . $action );
}
}

View File

@@ -0,0 +1,298 @@
<?php
namespace Elementor\Core\Base\Traits;
use Elementor\Controls_Manager;
use Elementor\Modules\FloatingButtons\Control\Hover_Animation_Floating_Buttons;
use Elementor\Plugin;
use Elementor\Shapes;
use Elementor\Utils;
trait Shared_Widget_Controls_Trait {
protected $border_width_range = [
'min' => 0,
'max' => 10,
'step' => 1,
];
protected function add_html_tag_control( string $control_name, string $default_tag = 'h2' ): void {
$this->add_control(
$control_name,
[
'label' => esc_html__( 'HTML Tag', 'elementor' ),
'type' => Controls_Manager::SELECT,
'options' => [
'h1' => 'H1',
'h2' => 'H2',
'h3' => 'H3',
'h4' => 'H4',
'h5' => 'H5',
'h6' => 'H6',
'div' => 'div',
'span' => 'span',
'p' => 'p',
],
'default' => $default_tag,
]
);
}
/**
* Remove any child arrays where all properties are empty
*/
protected function clean_array(
$input_array = []
) {
$output_array = array_filter( $input_array, function( $sub_array ) {
// Use array_filter on the sub array
$filtered_sub_array = array_filter( $sub_array, function( $val ) {
// Filter out empty or null values
return ! is_null( $val ) && '' !== $val;
} );
// A non-empty result means the sub array contains some non-empty value(s)
return ! empty( $filtered_sub_array );
} );
return $output_array;
}
protected function get_link_attributes(
$link = [],
$other_attributes = []
) {
$url_attrs = [];
$rel_string = '';
if ( ! empty( $link['url'] ) ) {
$url_attrs['href'] = esc_url( $link['url'] );
}
if ( ! empty( $link['is_external'] ) ) {
$url_attrs['target'] = '_blank';
$rel_string .= 'noopener ';
}
if ( ! empty( $link['nofollow'] ) ) {
$rel_string .= 'nofollow ';
}
if ( ! empty( $rel_string ) ) {
$url_attrs['rel'] = $rel_string;
}
/**
* Note - we deliberately merge $other_attributes second
* to allow overriding default attributes values such as a more formatted href
*/
$url_combined_attrs = array_merge(
$url_attrs,
$other_attributes,
Utils::parse_custom_attributes( $link['custom_attributes'] ?? '' ),
);
return $url_combined_attrs;
}
protected function add_icons_per_row_control(
string $name = 'icons_per_row',
$options = [
'2' => '2',
'3' => '3',
],
string $default_value = '3',
$label = '',
$selector_custom_property = '--e-link-in-bio-icon-columns'
): void {
if ( ! $label ) {
$label = esc_html__( 'Icons Per Row', 'elementor' );
}
$this->add_control(
$name,
[
'label' => $label,
'type' => Controls_Manager::SELECT,
'options' => $options,
'default' => $default_value,
'render_type' => 'template',
'selectors' => [
'{{WRAPPER}} .e-link-in-bio' => $selector_custom_property . ': {{VALUE}};',
],
]
);
}
protected function add_slider_control(
string $name,
array $args = []
): void {
$default_args = [
'type' => Controls_Manager::SLIDER,
'default' => [
'unit' => 'px',
],
'size_units' => [ 'px', '%', 'em', 'rem', 'vw', 'custom' ],
'range' => [
'px' => [
'min' => 0,
'max' => 100,
'step' => 1,
],
],
];
$this->add_control(
$name,
array_merge_recursive( $default_args, $args )
);
}
protected function add_borders_control(
string $prefix,
array $show_border_args = [],
array $border_width_args = [],
array $border_color_args = []
): void {
$show_border = [
'label' => esc_html__( 'Border', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'label_on' => esc_html__( 'Yes', 'elementor' ),
'label_off' => esc_html__( 'No', 'elementor' ),
'return_value' => 'yes',
'default' => '',
];
$this->add_control(
$prefix . '_show_border',
array_merge( $show_border, $show_border_args )
);
$condition = [
$prefix . '_show_border' => 'yes',
];
if ( isset( $border_width_args['condition'] ) ) {
$condition = array_merge( $condition, $border_width_args['condition'] );
unset( $border_width_args['condition'] );
}
$border_width = [
'label' => esc_html__( 'Border Width', 'elementor' ) . ' (px)',
'type' => Controls_Manager::SLIDER,
'size_units' => [ 'px' ],
'range' => [
'px' => $this->border_width_range,
],
'condition' => $condition,
'default' => [
'unit' => 'px',
'size' => 1,
],
];
$this->add_responsive_control(
$prefix . '_border_width',
array_merge( $border_width, $border_width_args ),
);
$condition = [
$prefix . '_show_border' => 'yes',
];
if ( isset( $border_color_args['condition'] ) ) {
$condition = array_merge( $condition, $border_color_args['condition'] );
unset( $border_color_args['condition'] );
}
$border_color = [
'label' => esc_html__( 'Border Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'condition' => $condition,
'default' => '#000000',
];
$this->add_control(
$prefix . '_border_color',
array_merge( $border_color, $border_color_args )
);
}
protected function get_shape_divider( $side = 'bottom' ) {
$settings = $this->settings;
$base_setting_key = "identity_section_style_cover_divider_$side";
$file_name = $settings[ $base_setting_key ];
if ( empty( $file_name ) ) {
return [];
}
$negative = ! empty( $settings[ $base_setting_key . '_negative' ] );
$shape_path = Shapes::get_shape_path( $file_name, $negative );
if ( ! is_file( $shape_path ) || ! is_readable( $shape_path ) ) {
return [];
}
return [
'negative' => $negative,
'svg' => Utils::file_get_contents( $shape_path ),
];
}
protected function print_shape_divider( $side = 'bottom' ) {
$shape_divider = $this->get_shape_divider( $side );
if ( empty( $shape_divider ) ) {
return;
}
?>
<div
class="elementor-shape elementor-shape-<?php echo esc_attr( $side ); ?>"
aria-hidden="true"
data-negative="<?php
echo esc_attr( $shape_divider['negative'] ? 'true' : 'false' );
?>"
>
<?php
// PHPCS - The file content is being read from a strict file path structure.
echo $shape_divider['svg']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</div>
<?php
}
protected function get_configured_breakpoints( $add_desktop = 'true' ) {
$active_devices = Plugin::$instance->breakpoints->get_active_devices_list( [ 'reverse' => true ] );
$active_breakpoint_instances = Plugin::$instance->breakpoints->get_active_breakpoints();
$devices_options = [];
foreach ( $active_devices as $device_key ) {
$device_label = 'desktop' === $device_key ? esc_html__( 'Desktop', 'elementor' ) : $active_breakpoint_instances[ $device_key ]->get_label();
$devices_options[ $device_key ] = $device_label;
}
return [
'active_devices' => $active_devices,
'devices_options' => $devices_options,
];
}
protected function add_hover_animation_control(
string $name,
array $args = []
): void {
$this->add_control(
$name,
array_merge(
[
'label' => esc_html__( 'Hover Animation', 'elementor' ),
'type' => Hover_Animation_Floating_Buttons::TYPE,
'frontend_available' => true,
'default' => 'grow',
],
$args
)
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\Core\Behaviors\Interfaces;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Lock_Behavior {
/**
* @return bool
*/
public function is_locked();
/**
* @return array {
*
* @type bool $is_locked
*
* @type array $badge {
* @type string $icon
* @type string $text
* }
*
* @type array $content {
* @type string $heading
* @type string $description
* }
*
* @type array $button {
* @type string $text
* @type string $url
* }
*
* }
*/
public function get_config();
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Elementor\Core\Breakpoints;
use Elementor\Core\Base\Base_Object;
use Elementor\Plugin;
use Elementor\Core\Breakpoints\Manager as Breakpoints_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Breakpoint extends Base_Object {
private $name;
private $label;
private $default_value;
private $db_key;
private $value;
private $is_custom;
private $direction = 'max';
private $is_enabled = false;
private $config;
/**
* Get Name
*
* @since 3.2.0
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Is Enabled
*
* Check if the breakpoint is enabled or not. The breakpoint instance receives this data from
* the Breakpoints Manager.
*
* @return bool $is_enabled class variable
*/
public function is_enabled() {
return $this->is_enabled;
}
/**
* Get Label
*
* Retrieve the breakpoint label.
*
* @since 3.2.0
*
* @return string $label class variable
*/
public function get_label() {
return $this->label;
}
/**
* Get Value
*
* Retrieve the saved breakpoint value.
*
* @since 3.2.0
*
* @return int $value class variable
*/
public function get_value() {
if ( ! $this->value ) {
$this->init_value();
}
return $this->value;
}
/**
* Is Custom
*
* Check if the breakpoint's value is a custom or default value.
*
* @since 3.2.0
*
* @return bool $is_custom class variable
*/
public function is_custom() {
if ( ! $this->is_custom ) {
$this->get_value();
}
return $this->is_custom;
}
/**
* Get Default Value
*
* Returns the Breakpoint's default value.
*
* @since 3.2.0
*
* @return int $default_value class variable
*/
public function get_default_value() {
return $this->default_value;
}
/**
* Get Direction
*
* Returns the Breakpoint's direction ('min'/'max').
*
* @since 3.2.0
*
* @return string $direction class variable
*/
public function get_direction() {
return $this->direction;
}
/**
* Set Value
*
* Set the `$value` class variable and the `$is_custom` class variable.
*
* @since 3.2.0
*
* @return int $value class variable
*/
private function init_value() {
$cached_value = Plugin::$instance->kits_manager->get_current_settings( $this->db_key );
if ( $cached_value ) {
$this->value = (int) $cached_value;
$this->is_custom = $this->value !== $this->default_value;
} else {
$this->value = $this->default_value;
$this->is_custom = false;
}
return $this->value;
}
public function __construct( $args ) {
$this->name = $args['name'];
$this->label = $args['label'];
// Used for CSS generation
$this->db_key = Breakpoints_Manager::BREAKPOINT_SETTING_PREFIX . $args['name'];
$this->direction = $args['direction'];
$this->is_enabled = $args['is_enabled'];
$this->default_value = $args['default_value'];
}
}

View File

@@ -0,0 +1,537 @@
<?php
namespace Elementor\Core\Breakpoints;
use Elementor\Core\Base\Module;
use Elementor\Core\Kits\Documents\Tabs\Settings_Layout;
use Elementor\Core\Responsive\Files\Frontend;
use Elementor\Modules\DevTools\Deprecation;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Manager extends Module {
const BREAKPOINT_SETTING_PREFIX = 'viewport_';
const BREAKPOINT_KEY_MOBILE = 'mobile';
const BREAKPOINT_KEY_MOBILE_EXTRA = 'mobile_extra';
const BREAKPOINT_KEY_TABLET = 'tablet';
const BREAKPOINT_KEY_TABLET_EXTRA = 'tablet_extra';
const BREAKPOINT_KEY_LAPTOP = 'laptop';
const BREAKPOINT_KEY_DESKTOP = 'desktop';
const BREAKPOINT_KEY_WIDESCREEN = 'widescreen';
/**
* Breakpoints
*
* An array containing instances of the all of the system's available breakpoints.
*
* @since 3.2.0
* @access private
*
* @var Breakpoint[]
*/
private $breakpoints;
/**
* Active Breakpoints
*
* An array containing instances of the enabled breakpoints.
*
* @since 3.2.0
* @access private
*
* @var Breakpoint[]
*/
private $active_breakpoints;
/**
* Responsive Control Duplication Mode.
*
* Determines the current responsive control generation mode.
* Options are:
* -- 'on': Responsive controls are duplicated in `add_responsive_control()`.
* -- 'off': Responsive controls are NOT duplicated in `add_responsive_control()`.
* -- 'dynamic': Responsive controls are only duplicated if their config contains `'dynamic' => 'active' => true`.
*
* When generating Post CSS, the mode is set to 'on'. When generating Dynamic CSS, the mode is set to 'dynamic'.
*
* default value is 'off'.
*
* @since 3.4.0
* @access private
*
* @var string
*/
private $responsive_control_duplication_mode = 'off';
private $icons_map;
/**
* Has Custom Breakpoints
*
* A flag that holds a cached value that indicates if there are active custom-breakpoints.
*
* @since 3.5.0
* @access private
*
* @var boolean
*/
private $has_custom_breakpoints;
public function get_name() {
return 'breakpoints';
}
/**
* Get Breakpoints
*
* Retrieve the array containing instances of all breakpoints existing in the system, or a single breakpoint if a
* name is passed.
*
* @since 3.2.0
*
* @param $breakpoint_name
* @return Breakpoint[]|Breakpoint
*/
public function get_breakpoints( $breakpoint_name = null ) {
if ( ! $this->breakpoints ) {
$this->init_breakpoints();
}
return self::get_items( $this->breakpoints, $breakpoint_name );
}
/**
* Get Active Breakpoints
*
* Retrieve the array of --enabled-- breakpoints, or a single breakpoint if a name is passed.
*
* @since 3.2.0
*
* @param string|null $breakpoint_name
* @return Breakpoint[]|Breakpoint
*/
public function get_active_breakpoints( $breakpoint_name = null ) {
if ( ! $this->active_breakpoints ) {
$this->init_active_breakpoints();
}
return self::get_items( $this->active_breakpoints, $breakpoint_name );
}
/**
* Get Active Devices List
*
* Retrieve an array containing the keys of all active devices, including 'desktop'.
*
* @since 3.2.0
*
* @param array $args
* @return array
*/
public function get_active_devices_list( $args = [] ) {
$default_args = [
'add_desktop' => true,
'reverse' => false,
'desktop_first' => false,
];
$args = array_merge( $default_args, $args );
$active_devices = array_keys( Plugin::$instance->breakpoints->get_active_breakpoints() );
if ( $args['add_desktop'] ) {
// Insert the 'desktop' device in the correct position.
if ( ! $args['desktop_first'] && in_array( 'widescreen', $active_devices, true ) ) {
$widescreen_index = array_search( 'widescreen', $active_devices, true );
array_splice( $active_devices, $widescreen_index, 0, [ 'desktop' ] );
} else {
$active_devices[] = 'desktop';
}
}
if ( $args['reverse'] ) {
$active_devices = array_reverse( $active_devices );
}
return $active_devices;
}
/** Has Custom Breakpoints
*
* Checks whether there are currently custom breakpoints saved in the database.
* Returns true if there are breakpoint values saved in the active kit.
* If the user has activated any additional custom breakpoints (mobile extra, tablet extra, laptop, widescreen) -
* that is considered as having custom breakpoints.
*
* @since 3.2.0
*
* @return boolean
*/
public function has_custom_breakpoints() {
if ( isset( $this->has_custom_breakpoints ) ) {
return $this->has_custom_breakpoints;
}
$breakpoints = $this->get_active_breakpoints();
$additional_breakpoints = [
self::BREAKPOINT_KEY_MOBILE_EXTRA,
self::BREAKPOINT_KEY_TABLET_EXTRA,
self::BREAKPOINT_KEY_LAPTOP,
self::BREAKPOINT_KEY_WIDESCREEN,
];
foreach ( $breakpoints as $breakpoint_name => $breakpoint ) {
if ( in_array( $breakpoint_name, $additional_breakpoints, true ) ) {
$this->has_custom_breakpoints = true;
return true;
}
/** @var Breakpoint $breakpoint */
if ( $breakpoint->is_custom() ) {
$this->has_custom_breakpoints = true;
return true;
}
}
$this->has_custom_breakpoints = false;
return false;
}
/**
* Get Device Min Breakpoint
*
* For a given device, return the minimum possible breakpoint. Except for the cases of mobile and widescreen
* devices, A device's min breakpoint is determined by the previous device's max breakpoint + 1px.
*
* @since 3.2.0
*
* @param string $device_name
* @return int the min breakpoint of the passed device
*/
public function get_device_min_breakpoint( $device_name ) {
if ( 'desktop' === $device_name ) {
return $this->get_desktop_min_point();
}
$active_breakpoints = $this->get_active_breakpoints();
$current_device_breakpoint = $active_breakpoints[ $device_name ];
// Since this method is called multiple times, usage of class variables is to memory and processing time.
// Get only the keys for active breakpoints.
$breakpoint_keys = array_keys( $active_breakpoints );
if ( $breakpoint_keys[0] === $device_name ) {
// For the lowest breakpoint, the min point is always 320.
$min_breakpoint = 320;
} elseif ( 'min' === $current_device_breakpoint->get_direction() ) {
// 'min-width' breakpoints only have a minimum point. The breakpoint value itself the device min point.
$min_breakpoint = $current_device_breakpoint->get_value();
} else {
// This block handles all other devices.
$device_name_index = array_search( $device_name, $breakpoint_keys, true );
$previous_index = $device_name_index - 1;
$previous_breakpoint_key = $breakpoint_keys[ $previous_index ];
/** @var Breakpoint $previous_breakpoint */
$previous_breakpoint = $active_breakpoints[ $previous_breakpoint_key ];
$min_breakpoint = $previous_breakpoint->get_value() + 1;
}
return $min_breakpoint;
}
/**
* Get Desktop Min Breakpoint
*
* Returns the minimum possible breakpoint for the default (desktop) device.
*
* @since 3.2.0
*
* @return int the min breakpoint of the passed device
*/
public function get_desktop_min_point() {
$active_breakpoints = $this->get_active_breakpoints();
$desktop_previous_device = $this->get_desktop_previous_device_key();
return $active_breakpoints[ $desktop_previous_device ]->get_value() + 1;
}
public function refresh() {
unset( $this->has_custom_breakpoints );
$this->init_breakpoints();
$this->init_active_breakpoints();
}
/**
* Get Responsive Icons Classes Map
*
* If a $device parameter is passed, this method retrieves the device's icon class list (the ones attached to the `<i>`
* element). If no parameter is passed, it returns an array of devices containing each device's icon class list.
*
* This method was created because 'mobile_extra' and 'tablet_extra' breakpoint icons need to be tilted by 90
* degrees, and this tilt is achieved in CSS via the class `eicon-tilted`.
*
* @since 3.4.0
*
* @return array|string
*/
public function get_responsive_icons_classes_map( $device = null ) {
if ( ! $this->icons_map ) {
$this->icons_map = [
'mobile' => 'eicon-device-mobile',
'mobile_extra' => 'eicon-device-mobile eicon-tilted',
'tablet' => 'eicon-device-tablet',
'tablet_extra' => 'eicon-device-tablet eicon-tilted',
'laptop' => 'eicon-device-laptop',
'desktop' => 'eicon-device-desktop',
'widescreen' => 'eicon-device-wide',
];
}
return self::get_items( $this->icons_map, $device );
}
/**
* Get Default Config
*
* Retrieve the default breakpoints config array. The 'selector' property is used for CSS generation (the
* Stylesheet::add_device() method).
*
* @return array
*/
public static function get_default_config() {
return [
self::BREAKPOINT_KEY_MOBILE => [
'label' => esc_html__( 'Mobile Portrait', 'elementor' ),
'default_value' => 767,
'direction' => 'max',
],
self::BREAKPOINT_KEY_MOBILE_EXTRA => [
'label' => esc_html__( 'Mobile Landscape', 'elementor' ),
'default_value' => 880,
'direction' => 'max',
],
self::BREAKPOINT_KEY_TABLET => [
'label' => esc_html__( 'Tablet Portrait', 'elementor' ),
'default_value' => 1024,
'direction' => 'max',
],
self::BREAKPOINT_KEY_TABLET_EXTRA => [
'label' => esc_html__( 'Tablet Landscape', 'elementor' ),
'default_value' => 1200,
'direction' => 'max',
],
self::BREAKPOINT_KEY_LAPTOP => [
'label' => esc_html__( 'Laptop', 'elementor' ),
'default_value' => 1366,
'direction' => 'max',
],
self::BREAKPOINT_KEY_WIDESCREEN => [
'label' => esc_html__( 'Widescreen', 'elementor' ),
'default_value' => 2400,
'direction' => 'min',
],
];
}
/**
* Get Breakpoints Config
*
* Iterates over an array of all of the system's breakpoints (both active and inactive), queries each breakpoint's
* class instance, and generates an array containing data on each breakpoint: its label, current value, direction
* ('min'/'max') and whether it is enabled or not.
*
* @return array
*/
public function get_breakpoints_config() {
$breakpoints = $this->get_breakpoints();
$config = [];
foreach ( $breakpoints as $breakpoint_name => $breakpoint ) {
$config[ $breakpoint_name ] = [
'label' => $breakpoint->get_label(),
'value' => $breakpoint->get_value(),
'default_value' => $breakpoint->get_default_value(),
'direction' => $breakpoint->get_direction(),
'is_enabled' => $breakpoint->is_enabled(),
];
}
return $config;
}
/**
* Get Responsive Control Duplication Mode
*
* Retrieve the value of the $responsive_control_duplication_mode private class variable.
* See the variable's PHPDoc for details.
*
* @since 3.4.0
* @access public
*/
public function get_responsive_control_duplication_mode() {
return $this->responsive_control_duplication_mode;
}
/**
* Set Responsive Control Duplication Mode
*
* Sets the value of the $responsive_control_duplication_mode private class variable.
* See the variable's PHPDoc for details.
*
* @since 3.4.0
*
* @access public
* @param string $mode
*/
public function set_responsive_control_duplication_mode( $mode ) {
$this->responsive_control_duplication_mode = $mode;
}
/**
* Get Stylesheet Templates Path
*
* @since 3.2.0
* @access public
* @static
*/
public static function get_stylesheet_templates_path() {
return ELEMENTOR_ASSETS_PATH . 'css/templates/';
}
/**
* Compile Stylesheet Templates
*
* @since 3.2.0
* @access public
* @static
*/
public static function compile_stylesheet_templates() {
foreach ( self::get_stylesheet_templates() as $file_name => $template_path ) {
$file = new Frontend( $file_name, $template_path );
$file->update();
}
}
/**
* Init Breakpoints
*
* Creates the breakpoints array, containing instances of each breakpoint. Returns an array of ALL breakpoints,
* both enabled and disabled.
*
* @since 3.2.0
*/
private function init_breakpoints() {
$breakpoints = [];
$setting_prefix = self::BREAKPOINT_SETTING_PREFIX;
$active_breakpoint_keys = [
$setting_prefix . self::BREAKPOINT_KEY_MOBILE,
$setting_prefix . self::BREAKPOINT_KEY_TABLET,
];
if ( Plugin::$instance->experiments->is_feature_active( 'additional_custom_breakpoints' ) ) {
$kit_active_id = Plugin::$instance->kits_manager->get_active_id();
// Get the breakpoint settings saved in the kit directly from the DB to avoid initializing the kit too early.
$raw_kit_settings = get_post_meta( $kit_active_id, '_elementor_page_settings', true );
// If there is an existing kit with an active breakpoints value saved, use it.
if ( isset( $raw_kit_settings[ Settings_Layout::ACTIVE_BREAKPOINTS_CONTROL_ID ] ) ) {
$active_breakpoint_keys = $raw_kit_settings[ Settings_Layout::ACTIVE_BREAKPOINTS_CONTROL_ID ];
}
}
$default_config = self::get_default_config();
foreach ( $default_config as $breakpoint_name => $breakpoint_config ) {
$args = [ 'name' => $breakpoint_name ] + $breakpoint_config;
// Make sure the two default breakpoints (mobile, tablet) are always enabled.
if ( self::BREAKPOINT_KEY_MOBILE === $breakpoint_name || self::BREAKPOINT_KEY_TABLET === $breakpoint_name ) {
// Make sure the default Mobile and Tablet breakpoints are always enabled.
$args['is_enabled'] = true;
} else {
// If the breakpoint is in the active breakpoints array, make sure it's instantiated as enabled.
$args['is_enabled'] = in_array( $setting_prefix . $breakpoint_name, $active_breakpoint_keys, true );
}
$breakpoints[ $breakpoint_name ] = new Breakpoint( $args );
}
$this->breakpoints = $breakpoints;
}
/**
* Init Active Breakpoints
*
* Create/Refresh the array of --enabled-- breakpoints.
*
* @since 3.2.0
*/
private function init_active_breakpoints() {
$this->active_breakpoints = array_filter( $this->get_breakpoints(), function( $breakpoint ) {
/** @var Breakpoint $breakpoint */
return $breakpoint->is_enabled();
} );
}
private function get_desktop_previous_device_key() {
$config_array_keys = array_keys( $this->get_active_breakpoints() );
$num_of_devices = count( $config_array_keys );
// If the widescreen breakpoint is active, the device that's previous to desktop is the last one before
// widescreen.
if ( self::BREAKPOINT_KEY_WIDESCREEN === $config_array_keys[ $num_of_devices - 1 ] ) {
$desktop_previous_device = $config_array_keys[ $num_of_devices - 2 ];
} else {
// If the widescreen breakpoint isn't active, we just take the last device returned by the config.
$desktop_previous_device = $config_array_keys[ $num_of_devices - 1 ];
}
return $desktop_previous_device;
}
/**
* Get Stylesheet Templates
*
* @since 3.2.0
* @access private
* @static
*/
private static function get_stylesheet_templates() {
$templates_paths = glob( self::get_stylesheet_templates_path() . '*.css' );
$templates = [];
foreach ( $templates_paths as $template_path ) {
$file_name = 'custom-' . basename( $template_path );
$templates[ $file_name ] = $template_path;
}
$deprecated_hook = 'elementor/core/responsive/get_stylesheet_templates';
$replacement_hook = 'elementor/core/breakpoints/get_stylesheet_template';
/**
* @type Deprecation $deprecation_module
*/
$deprecation_module = Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation;
// TODO: REMOVE THIS DEPRECATED HOOK IN ELEMENTOR v3.10.0/v4.0.0
$templates = $deprecation_module->apply_deprecated_filter( $deprecated_hook, [ $templates ], '3.2.0', $replacement_hook );
return apply_filters( $replacement_hook, $templates );
}
}

View File

@@ -0,0 +1,306 @@
<?php
namespace Elementor\Core\Common;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Common\Modules\Finder\Module as Finder;
use Elementor\Core\Common\Modules\Connect\Module as Connect;
use Elementor\Core\Common\Modules\EventTracker\Module as Event_Tracker;
use Elementor\Core\Common\Modules\EventsManager\Module as Events_Manager;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Icons_Manager;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* App
*
* Elementor's common app that groups shared functionality, components and configuration
*
* @since 2.3.0
*/
class App extends BaseApp {
private $templates = [];
/**
* App constructor.
*
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->add_default_templates();
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'register_scripts' ], 9 );
add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ], 9 );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ], 9 );
add_action( 'elementor/editor/before_enqueue_styles', [ $this, 'register_styles' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'register_styles' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_styles' ], 9 );
add_action( 'elementor/editor/footer', [ $this, 'print_templates' ] );
add_action( 'admin_footer', [ $this, 'print_templates' ] );
add_action( 'wp_footer', [ $this, 'print_templates' ] );
}
/**
* Init components
*
* Initializing common components.
*
* @since 2.3.0
* @access public
*/
public function init_components() {
$this->add_component( 'ajax', new Ajax() );
if ( current_user_can( 'manage_options' ) ) {
if ( ! is_customize_preview() ) {
$this->add_component( 'finder', new Finder() );
}
}
$this->add_component( 'connect', new Connect() );
$this->add_component( 'event-tracker', new Event_Tracker() );
Plugin::$instance->experiments->add_feature( Events_Manager::get_experimental_data() );
if ( Plugin::$instance->experiments->is_feature_active( Events_Manager::EXPERIMENT_NAME ) ) {
$this->add_component( 'events-manager', new Events_Manager() );
}
}
/**
* Get name.
*
* Retrieve the app name.
*
* @since 2.3.0
* @access public
*
* @return string Common app name.
*/
public function get_name() {
return 'common';
}
/**
* Register scripts.
*
* Register common scripts.
*
* @since 2.3.0
* @access public
*/
public function register_scripts() {
wp_register_script(
'elementor-common-modules',
$this->get_js_assets_url( 'common-modules' ),
[],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'backbone-marionette',
$this->get_js_assets_url( 'backbone.marionette', 'assets/lib/backbone/' ),
[
'backbone',
],
'2.4.5.e1',
true
);
wp_register_script(
'backbone-radio',
$this->get_js_assets_url( 'backbone.radio', 'assets/lib/backbone/' ),
[
'backbone',
],
'1.0.4',
true
);
wp_register_script(
'elementor-dialog',
$this->get_js_assets_url( 'dialog', 'assets/lib/dialog/' ),
[
'jquery-ui-position',
],
'4.9.0',
true
);
wp_enqueue_script(
'elementor-common',
$this->get_js_assets_url( 'common' ),
[
'jquery',
'jquery-ui-draggable',
'backbone-marionette',
'backbone-radio',
'elementor-common-modules',
'elementor-web-cli',
'elementor-dialog',
'wp-api-request',
'elementor-dev-tools',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'elementor-common', 'elementor' );
$this->print_config();
// Used for external plugins.
do_action( 'elementor/common/after_register_scripts', $this );
}
/**
* Register styles.
*
* Register common styles.
*
* @since 2.3.0
* @access public
*/
public function register_styles() {
wp_register_style(
'elementor-icons',
$this->get_css_assets_url( 'elementor-icons', 'assets/lib/eicons/css/' ),
[],
Icons_Manager::ELEMENTOR_ICONS_VERSION
);
wp_enqueue_style(
'elementor-common',
$this->get_css_assets_url( 'common', null, 'default', true ),
[
'elementor-icons',
],
ELEMENTOR_VERSION
);
wp_enqueue_style(
'e-theme-ui-light',
$this->get_css_assets_url( 'theme-light' ),
[],
ELEMENTOR_VERSION
);
}
/**
* Add template.
*
* @since 2.3.0
* @access public
*
* @param string $template Can be either a link to template file or template
* HTML content.
* @param string $type Optional. Whether to handle the template as path
* or text. Default is `path`.
*/
public function add_template( $template, $type = 'path' ) {
if ( 'path' === $type ) {
ob_start();
include $template;
$template = ob_get_clean();
}
$this->templates[] = $template;
}
/**
* Print Templates
*
* Prints all registered templates.
*
* @since 2.3.0
* @access public
*/
public function print_templates() {
foreach ( $this->templates as $template ) {
echo $template; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Get init settings.
*
* Define the default/initial settings of the common app.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$active_experimental_features = Plugin::$instance->experiments->get_active_features();
$all_experimental_features = Plugin::$instance->experiments->get_features();
$active_experimental_features = array_fill_keys( array_keys( $active_experimental_features ), true );
$all_experimental_features = array_map(
function( $feature ) {
return Plugin::$instance->experiments->is_feature_active( $feature['name'] );
},
$all_experimental_features
);
$config = [
'version' => ELEMENTOR_VERSION,
'isRTL' => is_rtl(),
'isDebug' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
'isElementorDebug' => Utils::is_elementor_debug(),
'activeModules' => array_keys( $this->get_components() ),
'experimentalFeatures' => $active_experimental_features,
'allExperimentalFeatures' => $all_experimental_features,
'urls' => [
'assets' => ELEMENTOR_ASSETS_URL,
'rest' => get_rest_url(),
],
'filesUpload' => [
'unfilteredFiles' => Uploads_Manager::are_unfiltered_uploads_enabled(),
],
'editor_events' => Events_Manager::get_editor_events_config(),
];
/**
* Localize common settings.
*
* Filters the editor localized settings.
*
* @since 1.0.0
*
* @param array $config Common configuration.
*/
return apply_filters( 'elementor/common/localize_settings', $config );
}
/**
* Add default templates.
*
* Register common app default templates.
*
* @since 2.3.0
* @access private
*/
private function add_default_templates() {
$default_templates = [
'includes/editor-templates/library-layout.php',
];
foreach ( $default_templates as $template ) {
$this->add_template( ELEMENTOR_PATH . $template );
}
}
}

View File

@@ -0,0 +1,316 @@
<?php
namespace Elementor\Core\Common\Modules\Ajax;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Utils\Exceptions;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor ajax manager.
*
* Elementor ajax manager handler class is responsible for handling Elementor
* ajax requests, ajax responses and registering actions applied on them.
*
* @since 2.0.0
*/
class Module extends BaseModule {
const NONCE_KEY = 'elementor_ajax';
/**
* Ajax actions.
*
* Holds all the register ajax action.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $ajax_actions = [];
/**
* Ajax requests.
*
* Holds all the register ajax requests.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $requests = [];
/**
* Ajax response data.
*
* Holds all the response data for all the ajax requests.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $response_data = [];
/**
* Current ajax action ID.
*
* Holds all the ID for the current ajax action.
*
* @since 2.0.0
* @access private
*
* @var string|null
*/
private $current_action_id = null;
/**
* Ajax manager constructor.
*
* Initializing Elementor ajax manager.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
add_action( 'wp_ajax_elementor_ajax', [ $this, 'handle_ajax_request' ] );
}
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'ajax';
}
/**
* Register ajax action.
*
* Add new actions for a specific ajax request and the callback function to
* be handle the response.
*
* @since 2.0.0
* @access public
*
* @param string $tag Ajax request name/tag.
* @param callable $callback The callback function.
*/
public function register_ajax_action( $tag, $callback ) {
if ( ! did_action( 'elementor/ajax/register_actions' ) ) {
_doing_it_wrong( __METHOD__, esc_html( sprintf( 'Use `%s` hook to register ajax action.', 'elementor/ajax/register_actions' ) ), '2.0.0' );
}
$this->ajax_actions[ $tag ] = compact( 'tag', 'callback' );
}
/**
* Handle ajax request.
*
* Verify ajax nonce, and run all the registered actions for this request.
*
* Fired by `wp_ajax_elementor_ajax` action.
*
* @since 2.0.0
* @access public
*/
public function handle_ajax_request() {
if ( ! $this->verify_request_nonce() ) {
$this->add_response_data( false, esc_html__( 'Token Expired.', 'elementor' ) )
->send_error( Exceptions::UNAUTHORIZED );
}
$editor_post_id = 0;
if ( ! empty( $_REQUEST['editor_post_id'] ) ) {
$editor_post_id = absint( $_REQUEST['editor_post_id'] );
Plugin::$instance->db->switch_to_post( $editor_post_id );
}
/**
* Register ajax actions.
*
* Fires when an ajax request is received and verified.
*
* Used to register new ajax action handles.
*
* @since 2.0.0
*
* @param self $this An instance of ajax manager.
*/
do_action( 'elementor/ajax/register_actions', $this );
if ( ! empty( $_REQUEST['actions'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, each action should sanitize its own data.
$this->requests = json_decode( wp_unslash( $_REQUEST['actions'] ), true );
}
foreach ( $this->requests as $id => $action_data ) {
$this->current_action_id = $id;
if ( ! isset( $this->ajax_actions[ $action_data['action'] ] ) ) {
$this->add_response_data( false, esc_html__( 'Action not found.', 'elementor' ), Exceptions::BAD_REQUEST );
continue;
}
if ( $editor_post_id ) {
$action_data['data']['editor_post_id'] = $editor_post_id;
}
try {
$data = $action_data['data'] ?? [];
$results = call_user_func( $this->ajax_actions[ $action_data['action'] ]['callback'], $data, $this );
if ( false === $results ) {
$this->add_response_data( false );
} else {
$this->add_response_data( true, $results );
}
} catch ( \Exception $e ) {
$this->add_response_data( false, $e->getMessage(), $e->getCode() );
}
}
$this->current_action_id = null;
$this->send_success();
}
/**
* Get current action data.
*
* Retrieve the data for the current ajax request.
*
* @since 2.0.1
* @access public
*
* @return bool|mixed Ajax request data if action exist, False otherwise.
*/
public function get_current_action_data() {
if ( ! $this->current_action_id ) {
return false;
}
return $this->requests[ $this->current_action_id ];
}
/**
* Create nonce.
*
* Creates a cryptographic token to
* give the user an access to Elementor ajax actions.
*
* @since 2.3.0
* @access public
*
* @return string The nonce token.
*/
public function create_nonce() {
return wp_create_nonce( self::NONCE_KEY );
}
/**
* Verify request nonce.
*
* Whether the request nonce verified or not.
*
* @since 2.3.0
* @access public
*
* @return bool True if request nonce verified, False otherwise.
*/
public function verify_request_nonce() {
return wp_verify_nonce( Utils::get_super_global_value( $_REQUEST, '_nonce' ), self::NONCE_KEY );
}
protected function get_init_settings() {
return [
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => $this->create_nonce(),
];
}
/**
* Ajax success response.
*
* Send a JSON response data back to the ajax request, indicating success.
*
* @since 2.0.0
* @access protected
*/
private function send_success() {
$response = [
'success' => true,
'data' => [
'responses' => $this->response_data,
],
];
$json = wp_json_encode( $response );
while ( ob_get_status() ) {
ob_end_clean();
}
header( 'Content-Type: application/json; charset=UTF-8' );
echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_die( '', '', [ 'response' => null ] );
}
/**
* Ajax failure response.
*
* Send a JSON response data back to the ajax request, indicating failure.
*
* @since 2.0.0
* @access protected
*
* @param null $code
*/
private function send_error( $code = null ) {
wp_send_json_error( [
'responses' => $this->response_data,
], $code );
}
/**
* Add response data.
*
* Add new response data to the array of all the ajax requests.
*
* @since 2.0.0
* @access protected
*
* @param bool $success True if the requests returned successfully, False
* otherwise.
* @param mixed $data Optional. Response data. Default is null.
*
* @param int $code Optional. Response code. Default is 200.
*
* @return Module An instance of ajax manager.
*/
private function add_response_data( $success, $data = null, $code = 200 ) {
$this->response_data[ $this->current_action_id ] = [
'success' => $success,
'code' => $code,
'data' => $data,
];
return $this;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Editor_One_Connect_Menu implements Menu_Item_Interface, Admin_Menu_Item_With_Page {
public function get_capability(): string {
return 'edit_posts';
}
public function get_parent_slug(): string {
return Menu_Config::ELEMENTOR_MENU_SLUG;
}
public function is_visible(): bool {
return false;
}
public function get_label(): string {
return esc_html__( 'Connect', 'elementor' );
}
public function get_position(): int {
return 999;
}
public function get_slug(): string {
return 'elementor-connect';
}
public function get_group_id(): string {
return Menu_Config::SYSTEM_GROUP_ID;
}
public function get_page_title() {
return esc_html__( 'Connect', 'elementor' );
}
public function render() {
$apps = Plugin::$instance->common->get_component( 'connect' )->get_apps();
?>
<style>
.elementor-connect-app-wrapper{
margin-bottom: 50px;
overflow: hidden;
}
</style>
<div class="wrap">
<?php
/** @var Base_App $app */
foreach ( $apps as $app ) {
echo '<div class="elementor-connect-app-wrapper">';
$app->render_admin_widget();
echo '</div>';
}
?>
</div><!-- /.wrap -->
<?php
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\Utils;
use Elementor\Modules\EditorOne\Classes\Menu_Data_Provider;
use Elementor\Core\Common\Modules\Connect\AdminMenuItems\Editor_One_Connect_Menu;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin {
const PAGE_ID = 'elementor-connect';
public static $url = '';
private function get_valid_redirect_to_from_request() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only reading a URL parameter.
$raw = Utils::get_super_global_value( $_GET, 'redirect_to' );
if ( ! $raw ) {
return '';
}
$raw = esc_url_raw( $raw );
$validated = wp_validate_redirect( $raw, '' );
if ( ! $validated ) {
return '';
}
$admin_host = wp_parse_url( admin_url(), PHP_URL_HOST );
$dest_host = wp_parse_url( $validated, PHP_URL_HOST );
if ( $dest_host && $admin_host && ! hash_equals( $admin_host, $dest_host ) ) {
return '';
}
return $validated;
}
public function register_admin_menu( Admin_Menu_Manager $admin_menu ) {
if ( ! $this->is_editor_one_active() ) {
$admin_menu->register( static::PAGE_ID, new Connect_Menu_Item() );
}
}
public function register_editor_one_menu( Menu_Data_Provider $menu_data_provider ) {
$menu_data_provider->register_menu( new Editor_One_Connect_Menu() );
}
private function is_editor_one_active(): bool {
return (bool) Plugin::instance()->modules_manager->get_modules( 'editor-one' );
}
/**
* @since 2.3.0
* @access public
*/
public function on_load_page() {
if ( ! $this->user_has_enough_permissions() ) {
wp_die( 'You do not have sufficient permissions to access this page.', 'You do not have sufficient permissions to access this page.', [
'back_link' => true,
] );
}
// Allow a per-request default landing URL when provided via a safe `redirect_to` parameter.
$maybe_redirect_to = $this->get_valid_redirect_to_from_request();
if ( $maybe_redirect_to ) {
self::$url = $maybe_redirect_to;
}
if ( isset( $_GET['action'], $_GET['app'] ) ) {
$manager = Plugin::$instance->common->get_component( 'connect' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$app_slug = Utils::get_super_global_value( $_GET, 'app' );
$app = $manager->get_app( $app_slug );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = Utils::get_super_global_value( $_GET, 'action' );
$nonce_action = $app_slug . $action;
if ( ! $app ) {
wp_die( 'Unknown app: ' . esc_attr( $app_slug ) );
}
if ( ! wp_verify_nonce( Utils::get_super_global_value( $_GET, 'nonce' ), $nonce_action ) ) {
wp_die( 'Invalid Nonce', 'Invalid Nonce', [
'back_link' => true,
] );
}
$method = 'action_' . $action;
if ( method_exists( $app, $method ) ) {
call_user_func( [ $app, $method ] );
}
}
}
private function user_has_enough_permissions() {
if ( current_user_can( 'manage_options' ) ) {
return true;
}
if ( 'library' === Utils::get_super_global_value( $_GET, 'app' ) ) {
return current_user_can( 'edit_posts' );
}
return false;
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
self::$url = admin_url( 'admin.php?page=' . self::PAGE_ID );
add_action( 'elementor/admin/menu/register', [ $this, 'register_admin_menu' ] );
add_action( 'elementor/editor-one/menu/register', [ $this, 'register_editor_one_menu' ] );
add_action( 'elementor/admin/menu/after_register', function ( Admin_Menu_Manager $admin_menu, array $hooks ) {
if ( ! empty( $hooks[ static::PAGE_ID ] ) ) {
add_action( 'load-' . $hooks[ static::PAGE_ID ], [ $this, 'on_load_page' ] );
}
}, 10, 2 );
add_action( 'elementor/editor-one/menu/after_register_hidden_submenus', function ( array $hooks ) {
if ( ! empty( $hooks[ static::PAGE_ID ] ) ) {
add_action( 'load-' . $hooks[ static::PAGE_ID ], [ $this, 'on_load_page' ] );
}
} );
}
}

View File

@@ -0,0 +1,865 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\Core\Admin\Admin_Notices;
use Elementor\Core\Common\Modules\Connect\Admin;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Http;
use Elementor\Core\Utils\Str;
use Elementor\Plugin;
use Elementor\Tracker;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_App {
const OPTION_NAME_PREFIX = 'elementor_connect_';
const OPTION_CONNECT_SITE_KEY = self::OPTION_NAME_PREFIX . 'site_key';
const SITE_URL = 'https://my.elementor.com/connect/v1';
const API_URL = 'https://my.elementor.com/api/connect/v1';
const HTTP_RETURN_TYPE_OBJECT = 'object';
const HTTP_RETURN_TYPE_ARRAY = 'array';
protected $data = [];
protected $auth_mode = '';
/**
* @var Http
*/
protected $http;
/**
* @since 2.3.0
* @access protected
* @abstract
* TODO: make it public.
*/
abstract protected function get_slug();
/**
* @since 2.8.0
* @access public
* TODO: make it abstract.
*/
public function get_title() {
return $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
* @abstract
*/
abstract protected function update_settings();
/**
* @since 2.3.0
* @access public
* @static
*/
public static function get_class_name() {
return get_called_class();
}
/**
* @access public
* @abstract
*/
public function render_admin_widget() {
// PHPCS - the method get_title return a plain string.
echo '<h2>' . $this->get_title() . '</h2>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ( $this->is_connected() ) {
$remote_user = $this->get( 'user' );
$title = sprintf(
/* translators: %s: Remote user. */
esc_html__( 'Connected as %s', 'elementor' ),
'<strong>' . esc_html( $remote_user->email ) . '</strong>'
);
$label = esc_html__( 'Disconnect', 'elementor' );
$url = $this->get_admin_url( 'disconnect' );
$attr = '';
printf(
'%s <a %s href="%s">%s</a>',
// PHPCS - the variable $title is already escaped above.
$title, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// PHPCS - the variable $attr is a plain string.
$attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
esc_attr( $url ),
esc_html( $label )
);
} else {
echo 'Not Connected';
}
echo '<hr>';
$this->print_app_info();
if ( current_user_can( 'manage_options' ) ) {
printf( '<div><a href="%s">%s</a></div>', esc_url( $this->get_admin_url( 'reset' ) ), esc_html__( 'Reset Data', 'elementor' ) );
}
echo '<hr>';
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_option_name() {
return static::OPTION_NAME_PREFIX . $this->get_slug();
}
/**
* @since 2.3.0
* @access public
*/
public function admin_notice() {
$notices = $this->get( 'notices' );
if ( ! $notices ) {
return;
}
$this->print_notices( $notices );
$this->delete( 'notices' );
}
public function get_app_token_from_cli_token( $cli_token ) {
$response = $this->request( 'get_app_token_from_cli_token', [
'cli_token' => $cli_token,
] );
if ( is_wp_error( $response ) ) {
// PHPCS - the variable $response does not contain a user input value.
wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
// Use state as usual.
$_REQUEST['state'] = $this->get( 'state' );
$_REQUEST['code'] = $response->code;
}
/**
* @since 2.3.0
* @access public
*/
public function action_authorize() {
if ( $this->is_connected() ) {
$this->add_notice( esc_html__( 'Already connected.', 'elementor' ), 'info' );
$this->redirect_to_admin_page();
return;
}
$this->set_client_id();
$this->set_request_state();
$this->redirect_to_remote_authorize_url();
}
public function action_reset() {
$this->redirect_to_admin_page();
}
/**
* @since 2.3.0
* @access public
*/
public function action_get_token() {
if ( $this->is_connected() ) {
$this->redirect_to_admin_page();
}
//phpcs:ignore WordPress.Security.NonceVerification.Recommended - The user as been authorized before in 'connect'.
$state = Utils::get_super_global_value( $_REQUEST, 'state' );
if ( $state !== $this->get( 'state' ) ) {
$this->add_notice( 'Get Token: Invalid Request.', 'error' );
$this->redirect_to_admin_page();
}
$response = $this->request( 'get_token', [
'grant_type' => 'authorization_code',
'code' => Utils::get_super_global_value( $_REQUEST, 'code' ), //phpcs:ignore WordPress.Security.NonceVerification.Recommended
'redirect_uri' => rawurlencode( $this->get_admin_url( 'get_token' ) ),
'client_id' => $this->get( 'client_id' ),
] );
if ( is_wp_error( $response ) ) {
$notice = 'Cannot Get Token:' . $response->get_error_message();
$this->add_notice( $notice, 'error' );
$this->redirect_to_admin_page();
}
$this->delete( 'state' );
$this->set( (array) $response );
if ( ! empty( $response->data_share_opted_in ) && current_user_can( 'manage_options' ) ) {
Tracker::set_opt_in( true );
}
$this->after_connect();
// Add the notice *after* the method `after_connect`, so an app can redirect without the notice.
$this->add_notice( esc_html__( 'Connected successfully.', 'elementor' ) );
$this->redirect_to_admin_page();
}
/**
* @since 2.3.0
* @access public
*/
public function action_disconnect() {
if ( $this->is_connected() ) {
$this->disconnect();
$this->add_notice( esc_html__( 'Disconnected successfully.', 'elementor' ) );
}
$this->redirect_to_admin_page();
}
/**
* @since 2.8.0
* @access public
*/
public function action_reconnect() {
$this->disconnect();
$this->action_authorize();
}
/**
* @since 2.3.0
* @access public
*/
public function get_admin_url( $action, $params = [] ) {
$params = [
'app' => $this->get_slug(),
'action' => $action,
'nonce' => wp_create_nonce( $this->get_slug() . $action ),
] + $params;
$admin_url = Str::encode_idn_url( get_admin_url() );
$admin_url .= 'admin.php?page=' . Admin::PAGE_ID;
return add_query_arg( $params, $admin_url );
}
/**
* @since 2.3.0
* @access public
*/
public function is_connected() {
return (bool) $this->get( 'access_token' );
}
/**
* @since 2.3.0
* @access protected
*/
protected function init() {}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {}
/**
* @since 2.3.0
* @access protected
*/
protected function after_connect() {}
/**
* @since 2.3.0
* @access public
*/
public function get( $key, $default_value = null ) {
$this->init_data();
return isset( $this->data[ $key ] ) ? $this->data[ $key ] : $default_value;
}
/**
* @since 2.3.0
* @access protected
*/
protected function set( $key, $value = null ) {
$this->init_data();
if ( is_array( $key ) ) {
$this->data = array_replace_recursive( $this->data, $key );
} else {
$this->data[ $key ] = $value;
}
$this->update_settings();
}
/**
* @since 2.3.0
* @access protected
*/
protected function delete( $key = null ) {
$this->init_data();
if ( $key ) {
unset( $this->data[ $key ] );
} else {
$this->data = [];
}
$this->update_settings();
}
/**
* @since 2.3.0
* @access protected
*/
protected function add( $key, $value, $default_value = '' ) {
$new_value = $this->get( $key, $default_value );
if ( is_array( $new_value ) ) {
$new_value[] = $value;
} elseif ( is_string( $new_value ) ) {
$new_value .= $value;
} elseif ( is_numeric( $new_value ) ) {
$new_value += $value;
}
$this->set( $key, $new_value );
}
/**
* @since 2.3.0
* @access protected
*/
protected function add_notice( $content, $type = 'success' ) {
$this->add( 'notices', compact( 'content', 'type' ), [] );
}
/**
* @param $action
* @param array $request_body
* @param false $as_array
*
* @return mixed|\WP_Error
*/
protected function request( $action, $request_body = [], $as_array = false ) {
$request_body = $this->get_connect_info() + $request_body;
return $this->http_request(
'POST',
$action,
[
'timeout' => 25,
'body' => $request_body,
'headers' => $this->is_connected() ?
[ 'X-Elementor-Signature' => $this->generate_signature( $request_body ) ] :
[],
],
[
'return_type' => $as_array ? static::HTTP_RETURN_TYPE_ARRAY : static::HTTP_RETURN_TYPE_OBJECT,
]
);
}
/**
* Get Base Connect Info
*
* Returns an array of connect info.
*
* @return array
*/
protected function get_base_connect_info() {
return [
'app' => $this->get_slug(),
'access_token' => $this->get( 'access_token' ),
'client_id' => $this->get( 'client_id' ),
'local_id' => get_current_user_id(),
'site_key' => $this->get_site_key(),
'home_url' => trailingslashit( home_url() ),
];
}
/**
* Get all the connect information
*
* @return array
*/
protected function get_connect_info() {
$connect_info = $this->get_base_connect_info();
$additional_info = [];
/**
* 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, $this );
return array_merge( $connect_info, $additional_info );
}
/**
* @param $endpoint
*
* @return array
*/
protected function generate_authentication_headers( $endpoint ) {
$connect_info = ( new Collection( $this->get_connect_info() ) )
->map_with_keys( function ( $value, $key ) {
// For bc `get_connect_info` returns the connect info with underscore,
// headers with underscore are not valid, so all the keys with underscore will be replaced to hyphen.
return [ str_replace( '_', '-', $key ) => $value ];
} )
->replace_recursive( [ 'endpoint' => $endpoint ] )
->sort_keys();
return $connect_info
->merge( [ 'X-Elementor-Signature' => $this->generate_signature( $connect_info->all() ) ] )
->all();
}
/**
* Send an http request
*
* @param $method
* @param $endpoint
* @param array $args
* @param array $options
*
* @return mixed|\WP_Error
*/
protected function http_request( $method, $endpoint, $args = [], $options = [] ) {
$options = wp_parse_args( $options, [
'return_type' => static::HTTP_RETURN_TYPE_OBJECT,
] );
$args = array_replace_recursive( [
'headers' => $this->is_connected() ? $this->generate_authentication_headers( $endpoint ) : [],
'method' => $method,
'timeout' => 10,
], $args );
$response = $this->http->request_with_fallback(
$this->get_generated_urls( $endpoint ),
$args
);
if ( is_wp_error( $response ) && empty( $options['with_error_data'] ) ) {
// PHPCS - the variable $response does not contain a user input value.
wp_die( $response, [ 'back_link' => true ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$body = wp_remote_retrieve_body( $response );
$response_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! $response_code ) {
return new \WP_Error( 500, 'No Response' );
}
// Server sent a success message without content.
if ( 'null' === $body ) {
$body = true;
}
$body = json_decode( $body, static::HTTP_RETURN_TYPE_ARRAY === $options['return_type'] );
if ( false === $body ) {
return new \WP_Error( 422, 'Wrong Server Response' );
}
if ( 201 === $response_code ) {
return $body;
}
if ( 200 !== $response_code ) {
// In case $as_array = true.
$body = (object) $body;
$message = isset( $body->message ) ? $body->message : wp_remote_retrieve_response_message( $response );
$code = (int) ( isset( $body->code ) ? $body->code : $response_code );
if ( ! $code ) {
$code = $response_code;
}
if ( 401 === $code ) {
$this->delete();
$should_retry = ! in_array( $this->auth_mode, [ 'xhr', 'cli' ], true );
if ( $should_retry ) {
$this->action_authorize();
}
}
if ( isset( $options['with_error_data'] ) && true === $options['with_error_data'] ) {
return new \WP_Error( $code, $message, $body );
}
return new \WP_Error( $code, $message );
}
return $body;
}
/**
* Create a signature for the http request
*
* @param array $payload
*
* @return false|string
*/
protected function generate_signature( $payload = [] ) {
return hash_hmac(
'sha256',
wp_json_encode( $payload, JSON_NUMERIC_CHECK ),
$this->get( 'access_token_secret' )
);
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_api_url() {
return static::API_URL . '/' . $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_remote_site_url() {
return static::SITE_URL . '/' . $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_remote_authorize_url() {
$redirect_uri = $this->get_auth_redirect_uri();
$allowed_query_params_to_propagate = [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'source',
'screen_hint',
];
$query_params = ( new Collection( $_GET ) ) // phpcs:ignore
->only( $allowed_query_params_to_propagate )
->merge( [
'action' => 'authorize',
'response_type' => 'code',
'client_id' => $this->get( 'client_id' ),
'auth_secret' => $this->get( 'auth_secret' ),
'state' => $this->get( 'state' ),
'redirect_uri' => rawurlencode( $redirect_uri ),
'may_share_data' => current_user_can( 'manage_options' ) && ! Tracker::is_allow_track(),
'reconnect_nonce' => wp_create_nonce( $this->get_slug() . 'reconnect' ),
] );
$utm_campaign = get_transient( 'elementor_core_campaign' );
if ( ! empty( $utm_campaign ) ) {
foreach ( [ 'source', 'medium', 'campaign' ] as $key ) {
if ( ! empty( $utm_campaign[ $key ] ) ) {
$query_params->offsetSet( 'utm_' . $key, $utm_campaign[ $key ] );
}
}
}
return add_query_arg( $query_params->all(), $this->get_remote_site_url() );
}
/**
* @since 2.3.0
* @access protected
*/
protected function redirect_to_admin_page( $url = '' ) {
if ( ! $url ) {
$url = Admin::$url;
}
switch ( $this->auth_mode ) {
case 'popup':
$this->print_popup_close_script( $url );
break;
case 'cli':
case 'rest':
$this->admin_notice();
die;
default:
wp_safe_redirect( $url );
die;
}
}
/**
* @since 2.3.0
* @access protected
*/
protected function set_client_id() {
$source = Utils::get_super_global_value( $_REQUEST, 'source' ) ?? ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
$response = $this->request(
'get_client_id',
[
'source' => esc_attr( $source ),
]
);
if ( is_wp_error( $response ) ) {
// PHPCS - the variable $response does not contain a user input value.
wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$this->set( 'client_id', $response->client_id );
$this->set( 'auth_secret', $response->auth_secret );
}
/**
* @since 2.3.0
* @access protected
*/
protected function set_request_state() {
$this->set( 'state', wp_generate_password( 12, false ) );
}
protected function get_popup_success_event_data() {
return [];
}
/**
* @since 2.3.0
* @access protected
*/
protected function print_popup_close_script( $url ) {
$data = $this->get_popup_success_event_data();
?>
<script>
if ( opener && opener !== window ) {
opener.jQuery( 'body' ).trigger(
'elementor/connect/success/<?php echo esc_attr( Utils::get_super_global_value( $_REQUEST, 'callback_id' ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here. ?>',
<?php echo wp_json_encode( $data ); ?>
);
opener.dispatchEvent( new CustomEvent( 'elementor/connect/success' ),
<?php echo wp_json_encode( $data ); ?>
);
window.close();
opener.focus();
} else {
location = '<?php echo esc_url( $url ); ?>';
}
</script>
<?php
die;
}
/**
* @since 2.3.0
* @access protected
*/
protected function disconnect() {
if ( $this->is_connected() ) {
// Try update the server, but not needed to handle errors.
$this->request( 'disconnect' );
}
$this->delete();
}
/**
* @since 2.3.0
* @access protected
*/
public function get_site_key() {
$site_key = get_option( static::OPTION_CONNECT_SITE_KEY );
if ( ! $site_key ) {
$site_key = md5( uniqid( wp_generate_password() ) );
update_option( static::OPTION_CONNECT_SITE_KEY, $site_key );
}
return $site_key;
}
protected function redirect_to_remote_authorize_url() {
switch ( $this->auth_mode ) {
case 'cli':
case 'rest':
$this->get_app_token_from_cli_token( Utils::get_super_global_value( $_REQUEST, 'token' ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
return;
default:
wp_redirect( $this->get_remote_authorize_url() ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Safe redirect is used here.
die;
}
}
protected function get_auth_redirect_uri() {
$redirect_uri = $this->get_admin_url( 'get_token' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
$val = Utils::get_super_global_value( $_REQUEST, 'redirect_to' );
if ( $val ) {
$redirect_uri = add_query_arg( [ 'redirect_to' => $val ], $redirect_uri );
}
switch ( $this->auth_mode ) {
case 'popup':
$redirect_uri = add_query_arg( [
'mode' => 'popup',
'callback_id' => esc_attr( Utils::get_super_global_value( $_REQUEST, 'callback_id' ) ), //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
], $redirect_uri );
break;
}
return $redirect_uri;
}
protected function print_notices( $notices ) {
switch ( $this->auth_mode ) {
case 'cli':
foreach ( $notices as $notice ) {
printf( '[%s] %s', wp_kses_post( $notice['type'] ), wp_kses_post( $notice['content'] ) );
}
break;
case 'rest':
// After `wp_send_json` the script will die.
$this->delete( 'notices' );
wp_send_json( $notices );
break;
default:
/**
* @var Admin_Notices $admin_notices
*/
$admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
foreach ( $notices as $notice ) {
$options = [
'description' => wp_kses_post( wpautop( $notice['content'] ) ),
'type' => $notice['type'],
'icon' => false,
];
$admin_notices->print_admin_notice( $options );
}
}
}
protected function get_app_info() {
return [];
}
protected function print_app_info() {
$app_info = $this->get_app_info();
foreach ( $app_info as $key => $item ) {
if ( $item['value'] ) {
$status = 'Exist';
$color = 'green';
} else {
$status = 'Empty';
$color = 'red';
}
// PHPCS - the values of $item['label'], $color, $status are plain strings.
printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
protected function get_generated_urls( $endpoint ) {
$base_urls = $this->get_api_url();
if ( ! is_array( $base_urls ) ) {
$base_urls = [ $base_urls ];
}
return array_map( function ( $base_url ) use ( $endpoint ) {
return trailingslashit( $base_url ) . $endpoint;
}, $base_urls );
}
private function init_auth_mode() {
$is_rest = defined( 'REST_REQUEST' ) && REST_REQUEST;
$is_ajax = wp_doing_ajax();
if ( $is_rest || $is_ajax ) {
// Set default to 'xhr' if rest or ajax request.
$this->set_auth_mode( 'xhr' );
}
$mode = Utils::get_super_global_value( $_REQUEST, 'mode' );
if ( $mode ) {
$allowed_auth_modes = [
'popup',
];
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$allowed_auth_modes[] = 'cli';
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
$allowed_auth_modes[] = 'rest';
}
if ( in_array( $mode, $allowed_auth_modes, true ) ) {
$this->set_auth_mode( $mode );
}
}
}
public function set_auth_mode( $mode ) {
$this->auth_mode = $mode;
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
add_action( 'admin_notices', [ $this, 'admin_notice' ] );
$this->init_auth_mode();
$this->http = new Http();
/**
* Allow extended apps to customize the __construct without call parent::__construct.
*/
$this->init();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_User_App extends Base_App {
/**
* @since 2.3.0
* @access protected
*/
protected function update_settings() {
update_user_option( get_current_user_id(), $this->get_option_name(), $this->data );
}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {
$this->data = get_user_option( $this->get_option_name() );
if ( ! $this->data ) {
$this->data = [];
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Common_App extends Base_User_App {
const OPTION_CONNECT_COMMON_DATA_KEY = self::OPTION_NAME_PREFIX . 'common_data';
protected static $common_data = null;
/**
* @since 2.3.0
* @access public
*/
public function get_option_name() {
return static::OPTION_NAME_PREFIX . 'common_data';
}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {
if ( is_null( self::$common_data ) ) {
self::$common_data = get_user_option( static::get_option_name() );
if ( ! self::$common_data ) {
self::$common_data = [];
}
}
$this->data = & self::$common_data;
}
public function action_reset() {
delete_user_option( get_current_user_id(), static::OPTION_CONNECT_COMMON_DATA_KEY );
parent::action_reset();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Connect extends Common_App {
public function get_title() {
return esc_html__( 'Connect', 'elementor' );
}
/**
* @since 2.3.0
* @access public
*/
protected function get_slug() {
return 'connect';
}
/**
* @since 2.3.0
* @access public
*/
public function render_admin_widget() {}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Feedback extends Common_App {
const EXPERIMENT_NAME = 'in_editor_feedback';
const FEEDBACK_ENDPOINT = 'https://my.elementor.com/feedback/api/v1';
public function __construct() {
parent::__construct();
Plugin::$instance->experiments->add_feature([
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'In-Editor Feedback', 'elementor' ),
'description' => esc_html__( 'Enable in-editor feedback submission.', 'elementor' ),
'hidden' => true,
'release_status' => Plugin::$instance->experiments::RELEASE_STATUS_BETA,
'default' => Plugin::$instance->experiments::STATE_INACTIVE,
'new_site' => [
'default_active' => false,
'minimum_installation_version' => '3.35.0',
],
]);
}
public function get_title() {
return esc_html__( 'Product Feedback', 'elementor' );
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_slug() {
return 'product-feedback';
}
protected function get_base_connect_info() {
return [
'app' => 'editor',
'access_token' => $this->get( 'access_token' ),
'client_id' => $this->get( 'client_id' ),
'endpoint' => 'taxonomies',
'local_id' => get_current_user_id(),
'site_key' => $this->get_site_key(),
'home_url' => trailingslashit( home_url() ),
];
}
protected function get_api_url() {
return static::FEEDBACK_ENDPOINT . '/' . $this->get_slug();
}
protected function get_generated_urls( $endpoint ) {
return [ $this->get_api_url() ];
}
public function submit( $body ) {
$is_active = Plugin::instance()->experiments->is_feature_active( self::EXPERIMENT_NAME );
if ( ! $is_active ) {
return [
'success' => false,
'data' => [
'message' => 'In-Editor Feedback is not active.',
],
];
}
$connect_info = $this->get_base_connect_info();
$merged_body = array_merge( $connect_info, $body );
$signature = $this->generate_signature( $merged_body );
$headers = [
'access-token' => $connect_info['access_token'],
'app' => 'library',
'client-id' => $connect_info['client_id'],
'endpoint' => 'taxonomies',
'home-url' => $connect_info['home_url'],
'local-id' => $connect_info['local_id'],
'site-key' => $this->get_site_key(),
'X-Elementor-Signature' => $signature,
];
$response = wp_remote_post( $this->get_api_url(), [
'headers' => $headers,
'body' => $body,
]);
return $response;
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\Api;
use Elementor\User;
use Elementor\Plugin;
use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Library extends Common_App {
public function get_title() {
return esc_html__( 'Library', 'elementor' );
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_slug() {
return 'library';
}
public function get_template_content( $id ) {
if ( ! $this->is_connected() ) {
return new \WP_Error( '401', esc_html__( 'Connecting to the Library failed. Please try reloading the page and try again', 'elementor' ) );
}
$body_args = [
'id' => $id,
// Which API version is used.
'api_version' => ELEMENTOR_VERSION,
// Which language to return.
'site_lang' => get_bloginfo( 'language' ),
];
/**
* API: Template body args.
*
* Filters the body arguments send with the GET request when fetching the content.
*
* @since 1.0.0
*
* @param array $body_args Body arguments.
*/
$body_args = apply_filters( 'elementor/api/get_templates/body_args', $body_args );
$template_content = $this->request( 'get_template_content', $body_args, true );
if ( is_wp_error( $template_content ) && 401 === $template_content->get_error_code() ) {
// Normalize 401 message
return new \WP_Error( 401, esc_html__( 'Connecting to the Library failed. Please try reloading the page and try again', 'elementor' ) );
}
return $template_content;
}
public function localize_settings( $settings ) {
$is_connected = $this->is_connected();
/** @var ConnectModule $connect */
$connect = Plugin::$instance->common->get_component( 'connect' );
$user_id = $this->get_user_id();
$user_roles = $this->get_user_roles();
$user = $this->get( 'user' );
return array_replace_recursive( $settings, [
'library_connect' => [
'is_connected' => $is_connected,
'user_id' => $user_id,
'user_roles' => $user_roles,
'subscription_plans' => $connect->get_subscription_plans( 'template-library' ),
// TODO: Remove `base_access_level`.
'base_access_level' => ConnectModule::ACCESS_LEVEL_CORE,
'base_access_tier' => ConnectModule::ACCESS_TIER_FREE,
'current_access_level' => ConnectModule::ACCESS_LEVEL_CORE,
'current_access_tier' => ConnectModule::ACCESS_TIER_FREE,
'plan_type' => ConnectModule::ACCESS_TIER_FREE,
'user_email' => $user->email ?? null,
],
] );
}
public function library_connect_popup_seen() {
User::set_introduction_viewed( [
'introductionKey' => 'library_connect',
] );
}
/**
* @param \Elementor\Core\Common\Modules\Ajax\Module $ajax_manager
*/
public function register_ajax_actions( $ajax_manager ) {
$ajax_manager->register_ajax_action( 'library_connect_popup_seen', [ $this, 'library_connect_popup_seen' ] );
}
private function get_user_id() {
$token = $this->get( 'access_token' );
if ( ! is_string( $token ) ) {
return null;
}
$parts = explode( '.', $token );
if ( count( $parts ) !== 3 ) {
return null;
}
try {
$payload_encoded = $parts[1];
$payload_encoded = str_pad( $payload_encoded, strlen( $payload_encoded ) + ( 4 - strlen( $payload_encoded ) % 4 ) % 4, '=' );
$payload_json = base64_decode( strtr( $payload_encoded, '-_', '+/' ), true );
$payload = json_decode( $payload_json, true );
if ( ! isset( $payload['sub'] ) ) {
return null;
}
return $payload['sub'];
} catch ( Exception $e ) {
error_log( 'JWT Decoding Error: ' . $e->getMessage() );
return null;
}
}
private function get_user_roles() {
$user = wp_get_current_user();
return $user->roles ?? [];
}
/**
* After Connect
*
* After Connecting to the library, re-fetch the library data to get it up to date.
*
* @since 3.7.0
*/
protected function after_connect() {
Api::get_library_data( true );
}
protected function get_app_info() {
return [
'user_common_data' => [
'label' => 'User Common Data',
'value' => get_user_option( $this->get_option_name(), get_current_user_id() ),
],
'connect_site_key' => [
'label' => 'Site Key',
'value' => get_option( self::OPTION_CONNECT_SITE_KEY ),
],
];
}
protected function get_popup_success_event_data() {
return [
'access_level' => ConnectModule::ACCESS_LEVEL_CORE,
'access_tier' => ConnectModule::ACCESS_TIER_FREE,
'plan_type' => ConnectModule::ACCESS_TIER_FREE,
'tracking_opted_in' => $this->get( 'data_share_opted_in' ) ?? false,
'user_id' => $this->get_user_id(),
];
}
protected function init() {
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_filter( 'elementor/common/localize_settings', [ $this, 'localize_settings' ] );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Plugin;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Connect_Menu_Item implements Admin_Menu_Item_With_Page {
public function is_visible() {
return false;
}
public function get_parent_slug() {
return Settings::PAGE_ID;
}
public function get_label() {
return esc_html__( 'Connect', 'elementor' );
}
public function get_page_title() {
return esc_html__( 'Connect', 'elementor' );
}
public function get_capability() {
return 'edit_posts';
}
public function render() {
$apps = Plugin::$instance->common->get_component( 'connect' )->get_apps();
?>
<style>
.elementor-connect-app-wrapper{
margin-bottom: 50px;
overflow: hidden;
}
</style>
<div class="wrap">
<?php
/** @var Base_App $app */
foreach ( $apps as $app ) {
echo '<div class="elementor-connect-app-wrapper">';
$app->render_admin_widget();
echo '</div>';
}
?>
</div><!-- /.wrap -->
<?php
}
}

View File

@@ -0,0 +1,271 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Common\Modules\Connect\Apps\Common_App;
use Elementor\Core\Common\Modules\Connect\Apps\Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Feedback;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Plugin;
use Elementor\Utils;
use WP_User_Query;
use Elementor\Core\Common\Modules\Connect\Rest\Rest_Api;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const ACCESS_LEVEL_CORE = 0;
const ACCESS_LEVEL_PRO = 1;
const ACCESS_LEVEL_EXPERT = 20;
const ACCESS_TIER_FREE = 'free';
const ACCESS_TIER_ESSENTIAL = 'essential';
const ACCESS_TIER_ESSENTIAL_OCT_2023 = 'essential-oct2023';
const ACCESS_TIER_ADVANCED = 'advanced';
const ACCESS_TIER_EXPERT = 'expert';
const ACCESS_TIER_AGENCY = 'agency';
const ACCESS_TIER_PRO_LEGACY = 'pro';
/**
* @since 2.3.0
* @access public
*/
public function get_name() {
return 'connect';
}
/**
* @var array
*/
protected $registered_apps = [];
/**
* Apps Instances.
*
* Holds the list of all the apps instances.
*
* @since 2.3.0
* @access protected
*
* @var Base_App[]
*/
protected $apps = [];
/**
* Registered apps categories.
*
* Holds the list of all the registered apps categories.
*
* @since 2.3.0
* @access protected
*
* @var array
*/
protected $categories = [];
protected $admin_page;
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->registered_apps = [
'connect' => Connect::get_class_name(),
'library' => Library::get_class_name(),
'feedback' => Feedback::get_class_name(),
];
// When using REST API the parent module is construct after the action 'elementor/init'
// so this part of code make sure to register the module "apps".
if ( did_action( 'elementor/init' ) ) {
$this->init();
} else {
// Note: The priority 11 is for allowing plugins to add their register callback on elementor init.
add_action( 'elementor/init', [ $this, 'init' ], 11 );
}
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
add_filter( 'elementor/tracker/send_tracking_data_params', function ( $params ) {
return $this->add_tracking_data( $params );
} );
}
/**
* Register default apps.
*
* Registers the default apps.
*
* @since 2.3.0
* @access public
*/
public function init() {
if ( is_admin() ) {
$this->admin_page = new Admin();
}
/**
* Register Elementor apps.
*
* Fires after Elementor registers the default apps.
*
* @since 2.3.0
*
* @param self $this The apps manager instance.
*/
do_action( 'elementor/connect/apps/register', $this );
foreach ( $this->registered_apps as $slug => $class ) {
$this->apps[ $slug ] = new $class();
}
}
/**
* Register app.
*
* Registers an app.
*
* @since 2.3.0
* @access public
*
* @param string $slug App slug.
* @param string $class_name App full class name.
*
* @return self The updated apps manager instance.
*/
public function register_app( $slug, $class_name ) {
$this->registered_apps[ $slug ] = $class_name;
return $this;
}
/**
* Get app instance.
*
* Retrieve the app instance.
*
* @since 2.3.0
* @access public
*
* @param $slug
*
* @return Base_App|null
*/
public function get_app( $slug ) {
if ( isset( $this->apps[ $slug ] ) ) {
return $this->apps[ $slug ];
}
return null;
}
/**
* @since 2.3.0
* @access public
* @return Base_App[]
*/
public function get_apps() {
return $this->apps;
}
/**
* @since 2.3.0
* @access public
*/
public function register_category( $slug, $args ) {
$this->categories[ $slug ] = $args;
return $this;
}
/**
* @since 2.3.0
* @access public
*/
public function get_categories() {
return $this->categories;
}
/**
* @param string $context Where this subscription plan should be shown.
*
* @return array
*/
public function get_subscription_plans( $context = '' ) {
$base_url = Utils::has_pro() ? 'https://my.elementor.com/upgrade-subscription' : 'https://elementor.com/pro';
$promotion_url = $base_url . '/?utm_source=' . $context . '&utm_medium=wp-dash&utm_campaign=gopro';
return [
static::ACCESS_TIER_FREE => [
'label' => null,
'promotion_url' => null,
'color' => null,
],
static::ACCESS_TIER_ESSENTIAL => [
'label' => 'Pro',
'promotion_url' => $promotion_url,
'color' => '#92003B',
],
static::ACCESS_TIER_ESSENTIAL_OCT_2023 => [
'label' => 'Advanced', // Should be the same label as "Advanced".
'promotion_url' => $promotion_url,
'color' => '#92003B',
],
static::ACCESS_TIER_ADVANCED => [
'label' => 'Advanced',
'promotion_url' => $promotion_url,
'color' => '#92003B',
],
static::ACCESS_TIER_EXPERT => [
'label' => 'Expert',
'promotion_url' => $promotion_url,
'color' => '#92003B',
],
static::ACCESS_TIER_AGENCY => [
'label' => 'Agency',
'promotion_url' => $promotion_url,
'color' => '#92003B',
],
];
}
private function add_tracking_data( $params ) {
$users = [];
$users_query = new WP_User_Query( [
'count_total' => false, // Disable SQL_CALC_FOUND_ROWS.
'meta_query' => [
'key' => Common_App::OPTION_CONNECT_COMMON_DATA_KEY,
'compare' => 'EXISTS',
],
] );
foreach ( $users_query->get_results() as $user ) {
$connect_common_data = get_user_option( Common_App::OPTION_CONNECT_COMMON_DATA_KEY, $user->ID );
if ( $connect_common_data ) {
$users [] = [
'id' => $user->ID,
'email' => $connect_common_data['user']->email,
'roles' => implode( ', ', $user->roles ),
];
}
}
$params['usages'][ $this->get_name() ] = [
'site_key' => get_option( Base_App::OPTION_CONNECT_SITE_KEY ),
'count' => count( $users ),
'users' => $users,
];
return $params;
}
public function register_rest_routes() {
$rest_api = new Rest_Api();
$rest_api->register_routes();
}
}

View File

@@ -0,0 +1,101 @@
# Elementor Library Connect REST API
This module provides REST API endpoints for connecting and disconnecting your WordPress site to the Elementor Library, similar in purpose to the [Elementor CLI Library Connect command](https://developers.elementor.com/docs/cli/library-connect/).
## Overview
The REST API allows programmatic connection and disconnection to the Elementor Library, which is useful for automation, integrations, and testing.
**Note:** The REST API is intended for internal and advanced use, mirroring the functionality of the CLI command.
## Endpoints
### 1. Connect to Elementor Library
- **URL:** `/index.php?rest_route=/elementor/v1/library/connect`
- **Method:** `POST`
- **Permissions:** Requires the `manage_options` capability (typically administrators).
- **Body Parameters:**
- `token` (string, required): The connect token from your Elementor account dashboard.
#### Example Request
```http
POST /index.php?rest_route=/elementor/v1/library/connect
Content-Type: application/json
Authorization: Basic {{encoded_wp_credentials}}
{
"token": "YOUR_CLI_TOKEN"
}
```
#### Example Success Response
```json
{
"success": true,
"message": "Connected successfully."
}
```
#### Example Error Response
```json
{
"code": "elementor_library_not_connected",
"message": "Failed to connect to Elementor Library.",
"data": {
"status": 500
}
}
```
---
### 2. Disconnect from Elementor Library
- **URL:** `/index.php?rest_route=/elementor/v1/library/connect`
- **Method:** `DELETE`
- **Permissions:** Requires the `manage_options` capability.
#### Example Request
```http
DELETE /index.php?rest_route=/elementor/v1/library/connect
Authorization: Basic {{encoded_wp_credentials}}
```
#### Example Success Response
```json
{
"success": true,
"message": "Disconnected successfully."
}
```
#### Example Error Response
```json
{
"code": "elementor_library_disconnect_error",
"message": "Error message here",
"data": {
"status": 500
}
}
```
---
## Permissions
All endpoints require the user to have the `manage_options` capability.
## Error Handling
Errors are returned as standard WordPress REST API error objects, with a `code`, `message`, and HTTP status.
## Reference
- For CLI usage and more context, see the [Elementor CLI Library Connect documentation](https://developers.elementor.com/docs/cli/library-connect/).

View File

@@ -0,0 +1,185 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Rest;
use Elementor\Plugin;
use WP_Http;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor Library Connect REST API.
*
* REST API controller for handling library connect operations.
*/
class Rest_Api {
/**
* REST API namespace.
*/
const REST_NAMESPACE = 'elementor/v1';
/**
* REST API base.
*/
const REST_BASE = 'library';
/**
* Authentication mode.
*/
const AUTH_MODE = 'rest';
/**
* Register REST API routes.
*
* @access public
* @return void
*/
public function register_routes() {
register_rest_route(
self::REST_NAMESPACE,
self::REST_BASE . '/connect',
[
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'connect' ],
'permission_callback' => [ $this, 'connect_permissions_check' ],
'args' => [
'token' => [
'required' => true,
'type' => 'string',
'description' => 'Connect CLI token',
],
],
],
]
);
register_rest_route(
self::REST_NAMESPACE,
self::REST_BASE . '/connect',
[
[
'methods' => \WP_REST_Server::DELETABLE,
'callback' => [ $this, 'disconnect' ],
'permission_callback' => [ $this, 'connect_permissions_check' ],
],
]
);
}
public function connect( \WP_REST_Request $request ) {
$app = $this->get_connect_app();
if ( ! $app ) {
return $this->elementor_library_app_not_available();
}
$app->set_auth_mode( self::AUTH_MODE );
$_REQUEST['mode'] = self::AUTH_MODE;
$_REQUEST['token'] = $request->get_param( 'token' );
try {
$app->action_authorize();
$app->action_get_token();
if ( $app->is_connected() ) {
return $this->success_response(
[ 'message' => __( 'Connected successfully.', 'elementor' ) ],
WP_Http::CREATED );
} else {
return $this->error_response(
'elementor_library_not_connected',
__( 'Failed to connect to Elementor Library.', 'elementor' ),
WP_Http::INTERNAL_SERVER_ERROR
);
}
} catch ( \Exception $e ) {
return $this->error_response(
'elementor_library_connect_error',
$e->getMessage(),
WP_Http::INTERNAL_SERVER_ERROR
);
}
}
public function disconnect( \WP_REST_Request $request ) {
$app = $this->get_connect_app();
if ( ! $app ) {
return $this->elementor_library_app_not_available();
}
$app->set_auth_mode( self::AUTH_MODE );
$_REQUEST['mode'] = self::AUTH_MODE;
try {
$app->action_disconnect();
return $this->success_response(
[ 'message' => __( 'Disconnected successfully.', 'elementor' ) ],
WP_Http::OK
);
} catch ( \Exception $e ) {
return $this->error_response(
'elementor_library_disconnect_error',
$e->getMessage(),
WP_Http::INTERNAL_SERVER_ERROR
);
}
}
public function connect_permissions_check( \WP_REST_Request $request ) {
return current_user_can( 'manage_options' );
}
private function route_wrapper( callable $cb ) {
try {
$response = $cb();
} catch ( \Exception $e ) {
return $this->error_response(
'unexpected_error',
__( 'Something went wrong', 'elementor' ),
WP_Http::INTERNAL_SERVER_ERROR
);
}
return $response;
}
private function error_response( $code, $message, $status = WP_Http::BAD_REQUEST ) {
return new \WP_Error(
$code,
$message,
[ 'status' => $status ]
);
}
private function success_response( $data = [], $status = WP_Http::OK ) {
$response = rest_ensure_response( array_merge( [ 'success' => true ], $data ) );
$response->set_status( $status );
return $response;
}
private function elementor_library_app_not_available() {
return $this->error_response(
'elementor_library_app_not_available',
__( 'Elementor Library app is not available.', 'elementor' ),
WP_Http::INTERNAL_SERVER_ERROR
);
}
/**
* Get the connect app.
*
* @return \Elementor\Core\Common\Modules\Connect\Apps\Library|null
*/
public function get_connect_app() {
$connect = Plugin::$instance->common->get_component( 'connect' );
if ( ! $connect ) {
return null;
}
$app = $connect->get_app( 'library' );
if ( ! $app ) {
$connect->init();
$app = $connect->get_app( 'library' );
}
return $app;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Elementor\Core\Common\Modules\EventTracker\Data;
use Elementor\Core\Common\Modules\EventTracker\DB as Events_DB_Manager;
use Elementor\Plugin;
use WP_REST_Server;
use Elementor\Data\V2\Base\Controller as Controller_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller extends Controller_Base {
public function get_name() {
return 'send-event';
}
public function register_endpoints() {
$this->index_endpoint->register_items_route( \WP_REST_Server::CREATABLE, [
'event_data' => [
'description' => 'All the recorded event data in JSON format',
'type' => 'object',
'required' => true,
],
] );
}
/**
* Get Permissions Callback
*
* This endpoint should only accept POST requests, and currently we only track site administrator actions.
*
* @since 3.6.0
*
* @param \WP_REST_Request $request
* @return bool
*/
public function get_permission_callback( $request ) {
if ( WP_REST_Server::CREATABLE !== $request->get_method() ) {
return false;
}
return current_user_can( 'manage_options' );
}
/**
* Create Items
*
* Receives a request for adding an event data entry into the database. If the request contains event data, this
* method initiates creation of a database entry with the event data in the Events DB table.
*
* @since 3.6.0
*
* @param \WP_REST_Request $request
* @return bool
*/
public function create_items( $request ) {
$request_body = $request->get_json_params();
if ( empty( $request_body['event_data'] ) ) {
return false;
}
/** @var Events_DB_Manager $event_tracker_db_manager */
$event_tracker_db_manager = Plugin::$instance->common
->get_component( 'event-tracker' )
->get_component( 'events-db' );
$event_tracker_db_manager->create_entry( $request_body['event_data'] );
return true;
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Elementor\Core\Common\Modules\EventTracker;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Common\Modules\Connect\Apps\Common_App;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class DB extends Base_Object {
/**
* @var \wpdb
*/
private $wpdb;
const TABLE_NAME = 'e_events';
const DB_VERSION_OPTION_KEY = 'elementor_events_db_version';
const CURRENT_DB_VERSION = '1.0.0';
/**
* Get Table Name
*
* Returns the Events database table's name with the `wpdb` prefix.
*
* @since 3.6.0
*
* @return string
*/
public function get_table_name() {
return $this->wpdb->prefix . self::TABLE_NAME;
}
/**
* Prepare Database for Entry
*
* The events database should have a limit of up to 1000 event entries stored daily.
* Before adding a new entry to the database, we make sure that the limit of 1000 events is not reached.
* If there are 1000 or more entries in the DB, we delete the earliest-inserted entry before inserting a new one.
*
* @since 3.6.0
*/
public function prepare_db_for_entry() {
$events = $this->get_event_ids_from_db();
if ( 1000 <= count( $events ) ) {
$event_ids = [];
foreach ( $events as $event ) {
$event_ids[] = $event->id;
}
// Sort the array by entry ID
array_multisort( $event_ids, SORT_ASC, $events );
// Delete the smallest ID (which is the earliest DB entry)
$this->wpdb->delete( $this->get_table_name(), [ 'ID' => $events[0]->id ] );
}
}
/**
* Create Entry
*
* Adds an event entry to the database.
*
* @since 3.6.0
*/
public function create_entry( $event_data ) {
$this->prepare_db_for_entry();
$connect = Plugin::$instance->common->get_component( 'connect' );
/** @var Library $library */
$library = $connect->get_apps()['library'];
if ( ! isset( $event_data['details'] ) ) {
$event_data['details'] = [];
}
if ( $library->is_connected() ) {
$user_connect_data = get_user_option( Common_App::OPTION_CONNECT_COMMON_DATA_KEY );
// Add the user's client ID to the event.
$event_data['details']['client_id'] = $user_connect_data['client_id'];
}
$event_data['details'] = wp_json_encode( $event_data['details'] );
$entry = [
'event_data' => wp_json_encode( $event_data ),
'created_at' => $event_data['ts'],
];
$this->wpdb->insert( $this->get_table_name(), $entry );
}
/**
* Get Event IDs From DB
*
* Fetches the IDs of all events saved in the database.
*
* @since 3.6.0
*
* @return array|object|null
*/
public function get_event_ids_from_db() {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $this->wpdb->get_results( "SELECT id FROM {$this->get_table_name()}" );
}
/**
* Reset Table
*
* Empties the contents of the Events DB table.
*
* @since 3.6.0
*/
public static function reset_table() {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
// Delete all content of the table.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "TRUNCATE TABLE {$table_name}" );
}
/**
* Create Table
*
* Creates the `wp_e_events` database table.
*
* @since 3.6.0
*
* @param string $query to that looks for the Events table in the DB. Used for checking if table was created.
*/
private function create_table( $query ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$table_name = $this->get_table_name();
$charset_collate = $this->wpdb->get_charset_collate();
$e_events_table = "CREATE TABLE `{$table_name}` (
id bigint(20) unsigned auto_increment primary key,
event_data text null,
created_at datetime not null
) {$charset_collate};";
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$this->wpdb->query( $e_events_table );
// Check if table was created successfully.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
if ( $this->wpdb->get_var( $query ) === $table_name ) {
update_option( self::DB_VERSION_OPTION_KEY, self::CURRENT_DB_VERSION, false );
}
}
/**
* Add Indexes
*
* Adds an index to the events table for the creation date column.
*
* @since 3.6.0
*/
private function add_indexes() {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$this->wpdb->query( 'ALTER TABLE ' . $this->get_table_name() . '
ADD INDEX `created_at_index` (`created_at`)
' );
}
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
// Check if table exists. If not, create it.
$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->get_table_name() ) );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
if ( $wpdb->get_var( $query ) !== $this->get_table_name() ) {
$this->create_table( $query );
$this->add_indexes();
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\Core\Common\Modules\EventTracker;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\EventTracker\Data\Controller;
use Elementor\Plugin;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Event Tracker Module Class
*
* @since 3.6.0
*/
class Module extends BaseModule {
public function get_name() {
return 'event-tracker';
}
/**
* Get init settings.
*
* @since 3.6.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
return [
'isUserDataShared' => Tracker::is_allow_track(),
];
}
public function __construct() {
// Initialize Events Database Table
$this->add_component( 'events-db', new DB() );
// Handle User Data Deletion/Export requests.
new Personal_Data();
Plugin::$instance->data_manager_v2->register_controller( new Controller() );
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Elementor\Core\Common\Modules\EventTracker;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Common\Modules\EventTracker\DB as Events_DB_Manager;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Personal_Data extends Base_Object {
const WP_KEY = 'elementor-event-tracker';
/**
* Get Title
*
* @since 3.6.0
*
* @return string
*/
private function get_title() {
return esc_html__( 'Elementor Event Tracker', 'elementor' );
}
/**
* Erase all the submissions related to specific email.
*
* Since event data is saved globally per site and not per user, we remove all saved events from the DB upon a
* user's data deletion request.
*
* @return array
*/
private function erase_data() {
// Get number of events saved in the DB.
/** @var Events_DB_Manager $event_tracker_db_manager */
$event_tracker_db_manager = Plugin::$instance->common
->get_component( 'event-tracker' )
->get_component( 'events-db' );
$events = $event_tracker_db_manager->get_event_ids_from_db();
$events_count = count( $events );
DB::reset_table();
// Validate table deleted
$updated_events = $event_tracker_db_manager->get_event_ids_from_db();
$updated_events_count = count( $updated_events );
return [
'items_removed' => $events_count - $updated_events_count,
'items_retained' => 0,
'messages' => [],
'done' => 0 === $updated_events_count,
];
}
/**
* Add eraser to the list of erasers.
*
* @param $erasers
*
* @return array[]
*/
private function add_eraser( $erasers ) {
return $erasers + [
self::WP_KEY => [
'eraser_friendly_name' => $this->get_title(),
'callback' => function () {
return $this->erase_data();
},
],
];
}
/**
* Personal_Data constructor.
*/
public function __construct() {
add_filter( 'wp_privacy_personal_data_erasers', function ( $exporters ) {
return $this->add_eraser( $exporters );
} );
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Elementor\Core\Common\Modules\EventsManager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Utils;
use Elementor\Plugin;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'editor_events';
const REMOTE_MIXPANEL_CONFIG_URL = 'https://assets.elementor.com/mixpanel/v1/mixpanel.json';
public function get_name() {
return 'events-manager';
}
public static function get_editor_events_config() {
$can_send_events = ! empty( ELEMENTOR_EDITOR_EVENTS_MIXPANEL_TOKEN ) &&
Tracker::is_allow_track() &&
! Tracker::has_terms_changed( '2025-07-07' ) &&
Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME );
$session_replays = [];
$is_flags_enabled = false;
if ( $can_send_events ) {
$mixpanel_config = self::get_remote_mixpanel_config();
$session_replays = $mixpanel_config[0]['sessionReplays'] ?? [];
$is_flags_enabled = $mixpanel_config[0]['flags'] ?? false;
}
$settings = [
'can_send_events' => $can_send_events,
'elementor_version' => ELEMENTOR_VERSION,
'site_url' => hash( 'sha256', get_site_url() ),
'wp_version' => get_bloginfo( 'version' ),
'user_agent' => esc_html( Utils::get_super_global_value( $_SERVER, 'HTTP_USER_AGENT' ) ),
'site_language' => get_locale(),
'site_key' => get_option( Base_App::OPTION_CONNECT_SITE_KEY ),
'subscription_id' => self::get_subscription_id(),
'subscription' => self::get_subscription(),
'token' => ELEMENTOR_EDITOR_EVENTS_MIXPANEL_TOKEN,
'session_replays' => $session_replays,
'flags_enabled' => $is_flags_enabled,
'isEditorOneActive' => Plugin::$instance->experiments->is_feature_active( 'e_editor_one' ),
];
return $settings;
}
public static function get_experimental_data(): array {
return [
'name' => static::EXPERIMENT_NAME,
'title' => esc_html__( 'Elementor Editor Events', 'elementor' ),
'description' => esc_html__( 'Editor events processing', 'elementor' ),
'hidden' => true,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
'default' => Experiments_Manager::STATE_INACTIVE,
'new_site' => [
'default_active' => true,
'minimum_installation_version' => '3.32.0',
],
];
}
private static function get_subscription_id() {
$subscription = self::get_subscription();
return $subscription['subscription_id'] ?? null;
}
private static function get_subscription() {
if ( ! Utils::has_pro() ) {
return null;
}
$license_data = get_option( '_elementor_pro_license_v2_data' );
if ( ! isset( $license_data['value'] ) ) {
return null;
}
return json_decode( $license_data['value'], true );
}
private static function get_remote_mixpanel_config() {
$data = wp_remote_get( static::REMOTE_MIXPANEL_CONFIG_URL );
if ( is_wp_error( $data ) ) {
return [];
}
$data = json_decode( wp_remote_retrieve_body( $data ), true );
if ( empty( $data['mixpanel'] ) || ! is_array( $data['mixpanel'] ) ) {
return [];
}
return $data['mixpanel'];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Core\Base\Base_Object;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Base Category
*
* Base class for Elementor Finder categories.
*/
abstract class Base_Category extends Base_Object {
/**
* Get title.
*
* @since 2.3.0
* @abstract
* @access public
*
* @return string
*/
abstract public function get_title();
/**
* Get a unique category ID.
*
* TODO: Make abstract.
*
* @since 3.5.0
* @deprecated 3.5.0
* @access public
*
* @return string
*/
public function get_id() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function(
get_class( $this ) . '::' . __FUNCTION__,
'3.5.0',
'This method will be replaced with an abstract method.'
);
return '';
}
/**
* Get category items.
*
* @since 2.3.0
* @abstract
* @access public
*
* @param array $options
*
* @return array
*/
abstract public function get_category_items( array $options = [] );
/**
* Is dynamic.
*
* Determine if the category is dynamic.
*
* @since 2.3.0
* @access public
*
* @return bool
*/
public function is_dynamic() {
return false;
}
/**
* Get init settings.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$settings = [
'title' => $this->get_title(),
'dynamic' => $this->is_dynamic(),
];
if ( ! $settings['dynamic'] ) {
$settings['items'] = $this->get_category_items();
}
return $settings;
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Categories_Manager {
/**
* @access private
*
* @var Base_Category[]
*/
private $categories;
/**
* @var array
*/
private $categories_list = [
'edit',
'general',
'create',
'site',
'settings',
'tools',
];
/**
* Add category.
*
* @since 2.3.0
* @deprecated 3.5.0 Use `register()` method instead.
* @access public
*
* @param string $category_name
* @param Base_Category $category
*
* @deprecated 3.5.0 Use `register()` method instead.
*/
public function add_category( $category_name, Base_Category $category ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function(
__METHOD__,
'3.5.0',
'register()'
);
$this->register( $category, $category_name );
}
/**
* Register finder category.
*
* @since 3.5.0
* @access public
*
* @param Base_Category $finder_category_instance An Instance of a category.
* @param string $finder_category_name A Category name. Deprecated parameter.
*
* @return void
*/
public function register( Base_Category $finder_category_instance, $finder_category_name = null ) {
// TODO: For BC. Remove in the future.
if ( $finder_category_name ) {
Plugin::instance()->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_argument(
'$finder_category_name', '3.5.0'
);
} else {
$finder_category_name = $finder_category_instance->get_id();
}
$this->categories[ $finder_category_name ] = $finder_category_instance;
}
/**
* Unregister a finder category.
*
* @param string $finder_category_name - Category to unregister.
*
* @return void
* @since 3.6.0
* @access public
*/
public function unregister( $finder_category_name ) {
unset( $this->categories[ $finder_category_name ] );
}
/**
* Get categories.
*
* Retrieve the registered categories, or a specific category if the category name
* is provided as a parameter.
*
* @since 2.3.0
* @access public
*
* @param string $category Category name.
*
* @return Base_Category|Base_Category[]|null
*/
public function get_categories( $category = '' ) {
if ( ! $this->categories ) {
$this->init_categories();
}
if ( $category ) {
if ( isset( $this->categories[ $category ] ) ) {
return $this->categories[ $category ];
}
return null;
}
return $this->categories;
}
/**
* Init categories.
*
* Used to initialize the native finder categories.
*
* @since 2.3.0
* @access private
*/
private function init_categories() {
foreach ( $this->categories_list as $category_name ) {
$class_name = __NAMESPACE__ . '\Categories\\' . $category_name;
$this->register( new $class_name() );
}
/**
* Elementor Finder categories init.
*
* Fires after Elementor Finder initialize it's native categories.
*
* This hook should be used to add your own Finder categories.
*
* @since 2.3.0
* @deprecated 3.5.0 Use `elementor/finder/register` hook instead.
*
* @param Categories_Manager $this.
*/
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->do_deprecated_action(
'elementor/finder/categories/init',
[ $this ],
'3.5.0',
'elementor/finder/register'
);
/**
* Elementor Finder categories registration.
*
* Fires after Elementor Finder initialize it's native categories.
*
* This hook should be used to register your own Finder categories.
*
* @since 3.5.0
*
* @param Categories_Manager $this Finder Categories manager.
*/
do_action( 'elementor/finder/register', $this );
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Create Category
*
* Provides items related to creation of new posts/pages/templates etc.
*/
class Create extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'Create', 'elementor' );
}
public function get_id() {
return 'create';
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$result = [];
$registered_document_types = Plugin::$instance->documents->get_document_types();
// TODO: Remove - Support 'post' backwards compatibility - See `Documents_Manager::register_default_types()`.
unset( $registered_document_types['post'] );
$elementor_supported_post_types = array_flip( get_post_types_by_support( 'elementor' ) );
foreach ( $registered_document_types as $document_name => $document_class ) {
$document_properties = $document_class::get_properties();
if ( empty( $document_properties['show_in_finder'] ) ) {
continue;
}
if ( ! empty( $document_properties['cpt'] ) ) {
foreach ( $document_properties['cpt'] as $cpt ) {
unset( $elementor_supported_post_types[ $cpt ] );
}
}
$result[ $document_name ] = $this->create_item_url_by_document_class( $document_class );
}
foreach ( $elementor_supported_post_types as $post_type => $val ) {
$result[ $post_type ] = $this->create_item_url_by_post_type( $post_type );
}
return $result;
}
private function create_item_url_by_post_type( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
// If there is an old post type from inactive plugins.
if ( ! $post_type_object ) {
return false;
}
return $this->get_create_new_template(
sprintf(
/* translators: %s: Post type singular name. */
__( 'Add New %s', 'elementor' ),
$post_type_object->labels->singular_name
),
Plugin::$instance->documents->get_create_new_post_url( $post_type )
);
}
private function create_item_url_by_document_class( $document_class ) {
$result = $this->get_create_new_template(
$document_class::get_add_new_title(),
$document_class::get_create_url()
);
$lock_behavior = $document_class::get_lock_behavior_v2();
$is_locked = ! empty( $lock_behavior ) && $lock_behavior->is_locked();
if ( $is_locked ) {
$result['lock'] = $lock_behavior->get_config();
}
return $result;
}
private function get_create_new_template( $add_new_title, $url ) {
return [
'title' => $add_new_title,
'icon' => 'plus-circle-o',
'url' => $url,
'keywords' => [ $add_new_title, 'post', 'page', 'template', 'new', 'create' ],
];
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Edit Category
*
* Provides items related to editing of posts/pages/templates etc.
*/
class Edit extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'Edit', 'elementor' );
}
public function get_id() {
return 'edit';
}
/**
* Is dynamic.
*
* Determine if the category is dynamic.
*
* @since 2.3.0
* @access public
*
* @return bool
*/
public function is_dynamic() {
return true;
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$post_types = get_post_types( [
'exclude_from_search' => false,
] );
$post_types[] = Source_Local::CPT;
$document_types = Plugin::$instance->documents->get_document_types( [
'is_editable' => true,
'show_in_finder' => true,
] );
$recently_edited_query_args = [
'no_found_rows' => true,
'post_type' => $post_types,
'post_status' => [ 'publish', 'draft', 'private', 'pending', 'future' ],
'posts_per_page' => '10',
'meta_query' => [
[
'key' => '_elementor_edit_mode',
'value' => 'builder',
],
[
'relation' => 'or',
[
'key' => Document::TYPE_META_KEY,
'compare' => 'NOT EXISTS',
],
[
'key' => Document::TYPE_META_KEY,
'value' => array_keys( $document_types ),
],
],
],
'orderby' => 'modified',
's' => $options['filter'],
];
$recently_edited_query = new \WP_Query( $recently_edited_query_args );
$items = [];
/** @var \WP_Post $post */
foreach ( $recently_edited_query->posts as $post ) {
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document ) {
continue;
}
$is_template = Source_Local::CPT === $post->post_type;
$description = $document->get_title();
$icon = 'document-file';
if ( $is_template ) {
$description = esc_html__( 'Template', 'elementor' ) . ' / ' . $description;
$icon = 'post-title';
}
$items[] = [
'icon' => $icon,
'title' => esc_html( $post->post_title ),
'description' => $description,
'url' => $document->get_edit_url(),
'actions' => [
[
'name' => 'view',
'url' => $document->get_permalink(),
'icon' => 'preview-medium',
],
],
];
}
return $items;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Core\RoleManager\Role_Manager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* General Category
*
* Provides general items related to Elementor Admin.
*/
class General extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'General', 'elementor' );
}
public function get_id() {
return 'general';
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'saved-templates' => [
'title' => esc_html__( 'Saved Templates', 'elementor' ),
'icon' => 'library-save',
'url' => Source_Local::get_admin_url(),
'keywords' => [ 'template', 'section', 'page', 'library' ],
],
'system-info' => [
'title' => esc_html__( 'System Info', 'elementor' ),
'icon' => 'info-circle-o',
'url' => admin_url( 'admin.php?page=elementor-system-info' ),
'keywords' => [ 'system', 'info', 'environment', 'elementor' ],
],
'role-manager' => [
'title' => esc_html__( 'Role Manager', 'elementor' ),
'icon' => 'person',
'url' => Role_Manager::get_url(),
'keywords' => [ 'role', 'manager', 'user', 'elementor' ],
],
'knowledge-base' => [
'title' => esc_html__( 'Knowledge Base', 'elementor' ),
'url' => admin_url( 'admin.php?page=go_knowledge_base_site' ),
'keywords' => [ 'help', 'knowledge', 'docs', 'elementor' ],
],
'theme-builder' => [
'title' => esc_html__( 'Theme Builder', 'elementor' ),
'icon' => 'library-save',
'url' => Plugin::$instance->app->get_settings( 'menu_url' ),
'keywords' => [ 'template', 'header', 'footer', 'single', 'archive', 'search', '404', 'library' ],
],
'kit-library' => [
'title' => esc_html__( 'Website Templates', 'elementor' ),
'icon' => 'kit-parts',
'url' => Plugin::$instance->app->get_base_url() . '&source=finder#/kit-library',
'keywords' => [ 'Website Templates', 'kit library', 'kit', 'library', 'site parts', 'parts', 'assets', 'templates' ],
],
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Modules\ElementManager\Module as ElementManagerModule;
use Elementor\Settings as ElementorSettings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Settings Category
*
* Provides items related to Elementor's settings.
*/
class Settings extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'Settings', 'elementor' );
}
public function get_id() {
return 'settings';
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'general-settings' => [
'title' => esc_html__( 'General Settings', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'general' ),
'keywords' => [ 'general', 'settings', 'elementor' ],
],
'integrations' => [
'title' => esc_html__( 'Integrations', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'integrations' ),
'keywords' => [ 'integrations', 'settings', 'elementor' ],
],
'advanced' => [
'title' => esc_html__( 'Advanced', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'advanced' ),
'keywords' => [ 'advanced', 'settings', 'elementor' ],
],
'performance' => [
'title' => esc_html__( 'Performance', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'performance' ),
'keywords' => [ 'performance', 'settings', 'elementor' ],
],
'experiments' => [
'title' => esc_html__( 'Experiments', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'experiments' ),
'keywords' => [ 'settings', 'elementor', 'experiments' ],
],
'features' => [
'title' => esc_html__( 'Features', 'elementor' ),
'url' => ElementorSettings::get_settings_tab_url( 'experiments' ),
'keywords' => [ 'settings', 'elementor', 'features' ],
],
'element-manager' => [
'title' => esc_html__( 'Element Manager', 'elementor' ),
'url' => admin_url( 'admin.php?page=' . ElementManagerModule::PAGE_ID ),
'keywords' => [ 'settings', 'elements', 'widgets', 'manager' ],
],
];
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Site Category
*
* Provides general site items.
*/
class Site extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'Site', 'elementor' );
}
public function get_id() {
return 'site';
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'homepage' => [
'title' => esc_html__( 'Homepage', 'elementor' ),
'url' => home_url(),
'icon' => 'home-heart',
'keywords' => [ 'home', 'page' ],
],
'wordpress-dashboard' => [
'title' => esc_html__( 'Dashboard', 'elementor' ),
'icon' => 'dashboard',
'url' => admin_url(),
'keywords' => [ 'dashboard', 'wordpress' ],
],
'wordpress-menus' => [
'title' => esc_html__( 'Menus', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'nav-menus.php' ),
'keywords' => [ 'menu', 'wordpress' ],
],
'wordpress-themes' => [
'title' => esc_html__( 'Themes', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'themes.php' ),
'keywords' => [ 'themes', 'wordpress' ],
],
'wordpress-customizer' => [
'title' => esc_html__( 'Customizer', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'customize.php' ),
'keywords' => [ 'customizer', 'wordpress' ],
],
'wordpress-plugins' => [
'title' => esc_html__( 'Plugins', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'plugins.php' ),
'keywords' => [ 'plugins', 'wordpress' ],
],
'wordpress-users' => [
'title' => esc_html__( 'Users', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'users.php' ),
'keywords' => [ 'users', 'profile', 'wordpress' ],
],
];
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Tools as ElementorTools;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tools Category
*
* Provides items related to Elementor's tools.
*/
class Tools extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return esc_html__( 'Tools', 'elementor' );
}
public function get_id() {
return 'tools';
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$tools_url = ElementorTools::get_url();
$items = [
'tools' => [
'title' => esc_html__( 'Tools', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url,
'keywords' => [ 'tools', 'regenerate css', 'safe mode', 'debug bar', 'sync library', 'elementor' ],
],
'replace-url' => [
'title' => esc_html__( 'Replace URL', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-replace_url',
'keywords' => [ 'tools', 'replace url', 'domain', 'elementor' ],
],
'maintenance-mode' => [
'title' => esc_html__( 'Maintenance Mode', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-maintenance_mode',
'keywords' => [ 'tools', 'maintenance', 'coming soon', 'elementor' ],
],
'import-export' => [
'title' => esc_html__( 'Import Export', 'elementor' ),
'icon' => 'import-export',
'url' => $tools_url . '#tab-import-export-kit',
'keywords' => [ 'tools', 'import export', 'import', 'export', 'kit' ],
],
];
if ( ElementorTools::can_user_rollback_versions() ) {
$items['version-control'] = [
'title' => esc_html__( 'Version Control', 'elementor' ),
'icon' => 'time-line',
'url' => $tools_url . '#tab-versions',
'keywords' => [ 'tools', 'version', 'control', 'rollback', 'beta', 'elementor' ],
];
}
return $items;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Finder Module
*
* Responsible for initializing Elementor Finder functionality
*/
class Module extends BaseModule {
/**
* Categories manager.
*
* @access private
*
* @var Categories_Manager
*/
private $categories_manager;
/**
* Module constructor.
*
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->categories_manager = new Categories_Manager();
$this->add_template();
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
/**
* Get name.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_name() {
return 'finder';
}
/**
* Add template.
*
* @since 2.3.0
* @access public
*/
public function add_template() {
Plugin::$instance->common->add_template( __DIR__ . '/template.php' );
}
/**
* Register ajax actions.
*
* @since 2.3.0
* @access public
*
* @param Ajax $ajax
*/
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'finder_get_category_items', [ $this, 'ajax_get_category_items' ] );
}
/**
* Ajax get category items.
*
* @since 2.3.0
* @access public
*
* @param array $data
*
* @return array
*
* @throws \Exception If finder category registration fails or validation errors occur.
*/
public function ajax_get_category_items( array $data ) {
if ( ! current_user_can( 'manage_options' ) ) {
throw new \Exception( 'Access denied.' );
}
$category = $this->categories_manager->get_categories( $data['category'] );
return $category->get_category_items( $data );
}
/**
* Get init settings.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$categories = $this->categories_manager->get_categories();
$categories_data = [];
foreach ( $categories as $category_name => $category ) {
$categories_data[ $category_name ] = array_merge( $category->get_settings(), [ 'name' => $category_name ] );
}
/**
* Finder categories.
*
* Filters the list of finder categories. This hook is used to manage Finder
* categories - to add new categories, remove and edit existing categories.
*
* @since 2.3.0
*
* @param array $categories_data A list of finder categories.
*/
$categories_data = apply_filters( 'elementor/finder/categories', $categories_data );
return [
'data' => $categories_data,
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\Finder;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-finder">
<div id="elementor-finder__search">
<i class="eicon-search" aria-hidden="true"></i>
<input id="elementor-finder__search__input" placeholder="<?php echo esc_attr__( 'Type to find anything in Elementor', 'elementor' ); ?>" autocomplete="off">
</div>
<div id="elementor-finder__content"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder-results-container">
<div id="elementor-finder__no-results"><?php echo esc_html__( 'No Results Found', 'elementor' ); ?></div>
<div id="elementor-finder__results"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder__results__category">
<div class="elementor-finder__results__category__title">{{{ title }}}</div>
<div class="elementor-finder__results__category__items"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder__results__item">
<a href="{{ url }}" class="elementor-finder__results__item__link">
<div class="elementor-finder__results__item__icon">
<i class="eicon-{{{ icon }}}" aria-hidden="true"></i>
</div>
<div class="elementor-finder__results__item__title">{{{ title }}}</div>
<# if ( description ) { #>
<div class="elementor-finder__results__item__description">- {{{ description }}}</div>
<# } #>
<# if ( lock ) { #>
<div class="elementor-finder__results__item__badge"><i class="{{{ lock.badge.icon }}}"></i>{{ lock.badge.text }}</div>
<# } #>
</a>
<# if ( actions.length ) { #>
<div class="elementor-finder__results__item__actions">
<# jQuery.each( actions, function() { #>
<a class="elementor-finder__results__item__action elementor-finder__results__item__action--{{ this.name }}" href="{{ this.url }}" target="_blank">
<i class="eicon-{{{ this.icon }}}"></i>
</a>
<# } ); #>
</div>
<# } #>
</script>

View File

@@ -0,0 +1,54 @@
<?php
namespace Elementor\Core\Database;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Database_Updater {
public function up( $force = false ) {
$installed_version = $this->get_installed_version();
if ( ! $force && $this->get_db_version() <= $installed_version ) {
return;
}
$migrations = new Collection( $this->get_migrations() );
if ( ! $force ) {
$migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) {
return $version > $installed_version;
} );
}
$migrations->map( function ( Base_Migration $migration, $version ) {
$migration->up();
$this->update_db_version_option( $version );
} );
$this->update_db_version_option( $this->get_db_version() );
}
public function register() {
add_action( 'admin_init', function () {
$this->up();
} );
}
protected function update_db_version_option( $version ) {
update_option( $this->get_db_version_option_name(), $version );
}
protected function get_installed_version() {
return intval( get_option( $this->get_db_version_option_name() ) );
}
abstract protected function get_db_version();
abstract protected function get_db_version_option_name(): string;
abstract protected function get_migrations(): array;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Elementor\Core\Database;
abstract class Base_Migration {
/**
* Runs when upgrading the database
*
* @return void
*/
abstract public function up();
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Elementor\Core\Debug\Classes;
use Elementor\Modules\SafeMode\Module as Safe_Mode;
use Elementor\Utils;
class Htaccess extends Inspection_Base {
private $message = '';
public function __construct() {
$this->message = esc_html__( 'Your site\'s .htaccess file appears to be missing.', 'elementor' );
}
public function run() {
$safe_mode_enabled = get_option( Safe_Mode::OPTION_ENABLED, '' );
if ( empty( $safe_mode_enabled ) || is_multisite() ) {
return true;
}
$permalink_structure = get_option( 'permalink_structure' );
if ( empty( $permalink_structure ) || empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
return true;
}
$server = strtoupper( Utils::get_super_global_value( $_SERVER, 'SERVER_SOFTWARE' ) );
if ( strstr( $server, 'APACHE' ) ) {
$htaccess_file = get_home_path() . '.htaccess';
/* translators: %s: Path to .htaccess file. */
$this->message .= ' ' . sprintf( esc_html__( 'File Path: %s', 'elementor' ), $htaccess_file ) . ' ';
return file_exists( $htaccess_file );
}
return true;
}
public function get_name() {
return 'apache-htaccess';
}
public function get_message() {
return $this->message;
}
public function get_help_doc_url() {
return 'https://go.elementor.com/preview-not-loaded/#htaccess';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\Core\Debug\Classes;
abstract class Inspection_Base {
/**
* @return bool
*/
abstract public function run();
/**
* @return string
*/
abstract public function get_name();
/**
* @return string
*/
abstract public function get_message();
/**
* @return string
*/
public function get_header_message() {
return esc_html__( 'The preview could not be loaded', 'elementor' );
}
/**
* @return string
*/
abstract public function get_help_doc_url();
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Core\Debug\Classes;
class Shop_Page_Edit extends Inspection_Base {
public function run() {
return false;
}
public function get_name() {
return 'shop-page-edit';
}
public function get_message() {
return esc_html__( 'You are trying to edit the Shop Page although it is a Product Archive. Use the Theme Builder to create your Shop Archive template instead', 'elementor' );
}
public function get_help_doc_url() {
return 'https://elementor.com/help/the-content-area-was-not-found-error/#error-appears-on-woocommerce-pages';
}
public function get_header_message() {
return esc_html__( 'Sorry, The content area was not been found on your page', 'elementor' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Core\Debug\Classes;
use Elementor\Modules\SafeMode\Module as Safe_Mode;
class Theme_Missing extends Inspection_Base {
public function run() {
$safe_mode_enabled = get_option( Safe_Mode::OPTION_ENABLED, '' );
if ( ! empty( $safe_mode_enabled ) ) {
return true;
}
$theme = wp_get_theme();
return $theme->exists();
}
public function get_name() {
return 'theme-missing';
}
public function get_message() {
return esc_html__( 'Some of your theme files are missing.', 'elementor' );
}
public function get_help_doc_url() {
return 'https://go.elementor.com/preview-not-loaded/#theme-files';
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Elementor\Core\Debug;
use Elementor\Settings;
use Elementor\Tools;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Inspector {
protected $is_enabled = false;
protected $log = [];
/**
* @since 2.1.2
* @access public
*/
public function __construct() {
$is_debug = ( defined( 'WP_DEBUG' ) && WP_DEBUG );
$option = get_option( 'elementor_enable_inspector', null );
$this->is_enabled = is_null( $option ) ? $is_debug : 'enable' === $option;
if ( $this->is_enabled ) {
add_action( 'admin_bar_menu', [ $this, 'add_menu_in_admin_bar' ], 201 );
}
add_action( 'elementor/admin/after_create_settings/' . Tools::PAGE_ID, [ $this, 'register_admin_tools_fields' ], 50 );
}
/**
* @since 2.1.3
* @access public
*/
public function is_enabled() {
return $this->is_enabled;
}
/**
* @since 2.1.3
* @access public
*/
public function register_admin_tools_fields( Tools $tools ) {
$tools->add_fields( Settings::TAB_GENERAL, 'tools', [
'enable_inspector' => [
'label' => esc_html__( 'Debug Bar', 'elementor' ),
'field_args' => [
'type' => 'select',
'std' => $this->is_enabled ? 'enable' : '',
'options' => [
'' => esc_html__( 'Disable', 'elementor' ),
'enable' => esc_html__( 'Enable', 'elementor' ),
],
'desc' => esc_html__( 'Debug Bar adds an admin bar menu that lists all the templates that are used on a page that is being displayed.', 'elementor' ),
],
],
] );
}
/**
* @since 2.1.2
* @access public
*/
public function parse_template_path( $template ) {
// `untrailingslashit` for windows path style.
if ( 0 === strpos( $template, untrailingslashit( ELEMENTOR_PATH ) ) ) {
return 'Elementor - ' . basename( $template );
}
if ( 0 === strpos( $template, get_stylesheet_directory() ) ) {
return wp_get_theme()->get( 'Name' ) . ' - ' . basename( $template );
}
$plugins_dir = dirname( ELEMENTOR_PATH );
if ( 0 === strpos( $template, $plugins_dir ) ) {
return ltrim( str_replace( $plugins_dir, '', $template ), '/\\' );
}
return str_replace( WP_CONTENT_DIR, '', $template );
}
/**
* @since 2.1.2
* @access public
*/
public function add_log( $module, $title, $url = '' ) {
if ( ! $this->is_enabled ) {
return;
}
if ( ! isset( $this->log[ $module ] ) ) {
$this->log[ $module ] = [];
}
$this->log[ $module ][] = [
'title' => $title,
'url' => $url,
];
}
/**
* @since 2.1.2
* @access public
*/
public function add_menu_in_admin_bar( \WP_Admin_Bar $wp_admin_bar ) {
if ( empty( $this->log ) ) {
return;
}
$wp_admin_bar->add_node( [
'id' => 'elementor_inspector',
'title' => esc_html__( 'Elementor Debugger', 'elementor' ),
] );
foreach ( $this->log as $module => $log ) {
$module_id = sanitize_key( $module );
$wp_admin_bar->add_menu( [
'id' => 'elementor_inspector_' . $module_id,
'parent' => 'elementor_inspector',
'title' => $module,
] );
foreach ( $log as $index => $row ) {
$url = $row['url'];
unset( $row['url'] );
$wp_admin_bar->add_menu( [
'id' => 'elementor_inspector_log_' . $module_id . '_' . $index,
'parent' => 'elementor_inspector_' . $module_id,
'href' => $url,
'title' => implode( ' > ', $row ),
'meta' => [
'target' => '_blank',
],
] );
}
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Elementor\Core\Debug;
use Elementor\Core\Debug\Classes\Inspection_Base;
use Elementor\Core\Debug\Classes\Shop_Page_Edit;
use Elementor\Core\Debug\Classes\Theme_Missing;
use Elementor\Core\Debug\Classes\Htaccess;
use Elementor\Utils;
class Loading_Inspection_Manager {
public static $_instance = null;
public static function instance() {
if ( null === self::$_instance ) {
self::$_instance = new Loading_Inspection_Manager();
}
return self::$_instance;
}
/** @var Inspection_Base[] */
private $inspections = [];
public function register_inspections() {
$this->inspections['theme-missing'] = new Theme_Missing();
$this->inspections['htaccess'] = new Htaccess();
$is_editing_shop_page = Utils::get_super_global_value( $_GET, 'post' ) == get_option( 'woocommerce_shop_page_id' );
if ( $is_editing_shop_page ) {
$this->inspections['shop-page-edit'] = new Shop_Page_Edit();
}
}
/**
* @param Inspection_Base $inspection
*/
public function register_inspection( $inspection ) {
$this->inspections[ $inspection->get_name() ] = $inspection;
}
public function run_inspections() {
$debug_data = [
'message' => esc_html__( "Were sorry, but something went wrong. Click on 'Learn more' and follow each of the steps to quickly solve it.", 'elementor' ),
'header' => esc_html__( 'The preview could not be loaded', 'elementor' ),
'doc_url' => 'https://go.elementor.com/preview-not-loaded/',
];
foreach ( $this->inspections as $inspection ) {
if ( ! $inspection->run() ) {
$debug_data = [
'message' => $inspection->get_message(),
'header' => $inspection->get_header_message(),
'doc_url' => $inspection->get_help_doc_url(),
'error' => true,
];
break;
}
}
return $debug_data;
}
}

View File

@@ -0,0 +1,332 @@
<?php
namespace Elementor\Core\DocumentTypes;
use Elementor\Controls_Manager;
use Elementor\Core\Base\Document;
use Elementor\Group_Control_Background;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class PageBase extends Document {
/**
* Get Properties
*
* Return the document configuration properties.
*
* @since 2.0.8
* @access public
* @static
*
* @return array
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['admin_tab_group'] = '';
$properties['support_wp_page_templates'] = true;
return $properties;
}
/**
* @since 2.1.2
* @access protected
* @static
*/
protected static function get_editor_panel_categories() {
return Utils::array_inject(
parent::get_editor_panel_categories(),
'theme-elements',
[
'theme-elements-single' => [
'title' => esc_html__( 'Single', 'elementor' ),
'active' => false,
'promotion' => [
'url' => esc_url( 'https://go.elementor.com/go-pro-section-single-widget-panel/' ),
],
],
]
);
}
/**
* @since 2.0.0
* @access public
*/
public function get_css_wrapper_selector() {
return 'body.elementor-page-' . $this->get_main_id();
}
/**
* @since 3.1.0
* @access protected
*/
protected function register_controls() {
parent::register_controls();
static::register_hide_title_control( $this );
static::register_post_fields_control( $this );
static::register_style_controls( $this );
}
/**
* @since 2.0.0
* @access public
* @static
* @param Document $document
*/
public static function register_hide_title_control( $document ) {
$document->start_injection( [
'of' => 'post_status',
'fallback' => [
'of' => 'post_title',
],
] );
$document->add_control(
'hide_title',
[
'label' => esc_html__( 'Hide Title', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'description' => sprintf(
/* translators: 1: Link open tag, 2: Link close tag. */
esc_html__( 'Set a different selector for the title in the %1$sLayout panel%2$s.', 'elementor' ),
'<a href="javascript: $e.run( \'panel/global/open\' ).then( () => $e.route( \'panel/global/settings-layout\' ) )">',
'</a>'
),
'separator' => 'before',
'selectors' => [
':root' => '--page-title-display: none',
],
]
);
$document->end_injection();
}
/**
* @since 2.0.0
* @access public
* @static
* @param Document $document
*/
public static function register_style_controls( $document ) {
$document->start_controls_section(
'section_page_style',
[
'label' => esc_html__( 'Body Style', 'elementor' ),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$document->add_responsive_control(
'margin',
[
'label' => esc_html__( 'Margin', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%', 'em', 'rem', 'vw', 'custom' ],
'selectors' => [
'{{WRAPPER}}' => 'margin: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}',
],
]
);
$document->add_responsive_control(
'padding',
[
'label' => esc_html__( 'Padding', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%', 'em', 'rem', 'vw', 'custom' ],
'selectors' => [
'{{WRAPPER}}' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}',
],
]
);
$document->add_group_control(
Group_Control_Background::get_type(),
[
'name' => 'background',
'separator' => 'before',
'fields_options' => [
'image' => [
// Currently isn't supported.
'dynamic' => [
'active' => false,
],
],
],
]
);
$document->end_controls_section();
Plugin::$instance->controls_manager->add_custom_css_controls( $document );
}
public static function get_labels(): array {
$plural_label = static::get_plural_title();
$singular_label = static::get_title();
$labels = [
'name' => $plural_label, // Already translated.
'singular_name' => $singular_label, // Already translated.
'all_items' => sprintf(
/* translators: 1: Plural label. */
__( 'All %s', 'elementor' ),
$plural_label
),
'add_new' => esc_html__( 'Add New', 'elementor' ),
'add_new_item' => sprintf(
/* translators: %s: Singular label. */
__( 'Add New %s', 'elementor' ),
$singular_label
),
'edit_item' => sprintf(
/* translators: %s: Singular label. */
__( 'Edit %s', 'elementor' ),
$singular_label
),
'new_item' => sprintf(
/* translators: %s: Singular label. */
__( 'New %s', 'elementor' ),
$singular_label
),
'view_item' => sprintf(
/* translators: %s: Singular label. */
__( 'View %s', 'elementor' ),
$singular_label
),
'search_items' => sprintf(
/* translators: %s: Plural label. */
__( 'Search %s', 'elementor' ),
$plural_label
),
'not_found' => sprintf(
/* translators: %s: Plural label. */
__( 'No %s found.', 'elementor' ),
strtolower( $plural_label )
),
'not_found_in_trash' => sprintf(
/* translators: %s: Plural label. */
__( 'No %s found in Trash.', 'elementor' ),
strtolower( $plural_label )
),
'parent_item_colon' => '',
'menu_name' => $plural_label,
];
return $labels;
}
/**
* @since 2.0.0
* @access public
* @static
* @param Document $document
*/
public static function register_post_fields_control( $document ) {
$document->start_injection( [
'of' => 'post_status',
'fallback' => [
'of' => 'post_title',
],
] );
if ( post_type_supports( $document->post->post_type, 'excerpt' ) ) {
$document->add_control(
'post_excerpt',
[
'label' => esc_html__( 'Excerpt', 'elementor' ),
'type' => Controls_Manager::TEXTAREA,
'default' => $document->post->post_excerpt,
'separator' => 'before',
'ai' => [
'type' => 'excerpt',
],
]
);
}
if ( current_theme_supports( 'post-thumbnails' ) && post_type_supports( $document->post->post_type, 'thumbnail' ) ) {
$document->add_control(
'post_featured_image',
[
'label' => esc_html__( 'Featured Image', 'elementor' ),
'type' => Controls_Manager::MEDIA,
'default' => [
'id' => get_post_thumbnail_id(),
'url' => (string) get_the_post_thumbnail_url( $document->post->ID ),
],
'separator' => 'before',
]
);
}
if ( is_post_type_hierarchical( $document->post->post_type ) ) {
$document->add_control(
'menu_order',
[
'label' => esc_html__( 'Order', 'elementor' ),
'type' => Controls_Manager::NUMBER,
'default' => $document->post->menu_order,
'separator' => 'before',
]
);
}
if ( post_type_supports( $document->post->post_type, 'comments' ) ) {
$document->add_control(
'comment_status',
[
'label' => esc_html__( 'Allow Comments', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'return_value' => 'open',
'default' => $document->post->comment_status,
'separator' => 'before',
]
);
}
$document->end_injection();
}
/**
* @since 2.0.0
* @access public
*
* @param array $data
*
* @throws \Exception If the post ID is not set.
*/
public function __construct( array $data = [] ) {
if ( $data ) {
$template = get_post_meta( $data['post_id'], '_wp_page_template', true );
if ( empty( $template ) ) {
$template = 'default';
}
$data['settings']['template'] = $template;
}
parent::__construct( $data );
}
protected function get_remote_library_config() {
$config = parent::get_remote_library_config();
$config['category'] = '';
$config['type'] = 'block';
$config['default_route'] = 'templates/blocks';
return $config;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Elementor\Core\DocumentTypes;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Page extends PageBase {
const URL_TYPE = 'site_settings';
const SITE_IDENTITY_TAB = 'settings-site-identity';
/**
* Get Properties
*
* Return the page document configuration properties.
*
* @access public
* @static
*
* @return array
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['cpt'] = [ 'page' ];
$properties['support_kit'] = true;
return $properties;
}
/**
* Get Type
*
* Return the page document type.
*
* @return string
*/
public static function get_type() {
return 'wp-page';
}
/**
* Get Title
*
* Return the page document title.
*
* @access public
* @static
*
* @return string
*/
public static function get_title() {
return esc_html__( 'Page', 'elementor' );
}
/**
* Get Plural Title
*
* Return the page document plural title.
*
* @access public
* @static
*
* @return string
*/
public static function get_plural_title() {
return esc_html__( 'Pages', 'elementor' );
}
public static function get_site_settings_url_config( $active_tab_id = null ) {
$existing_elementor_page = self::get_elementor_page();
$site_settings_url = $existing_elementor_page
? self::get_elementor_edit_url( $existing_elementor_page->ID, [ 'active-tab' => $active_tab_id ] )
: self::get_create_new_editor_page_url( $active_tab_id );
return [
'new_page' => empty( $existing_elementor_page ),
'url' => $site_settings_url,
'type' => static::URL_TYPE,
];
}
public static function get_create_new_editor_page_url( $active_tab = null ): string {
$active_kit_id = Plugin::$instance->kits_manager->get_active_id();
$args = [];
if ( ! empty( $active_kit_id ) ) {
$args['active-document'] = $active_kit_id;
}
if ( $active_tab ) {
$args['active-tab'] = $active_tab;
}
return add_query_arg( $args, Plugin::$instance->documents->get_create_new_post_url( 'page' ) );
}
private static function get_elementor_edit_url( int $post_id, $args = [] ): string {
$page = new self( [ 'post_id' => $post_id ] );
$url = add_query_arg( $args, $page->get_edit_url() );
if ( Plugin::$instance->kits_manager->get_active_id() ) {
return $url . '#e:run:panel/global/open';
}
return $url;
}
public static function get_elementor_page() {
return get_pages( [
'post_status' => [ 'publish', 'draft' ],
'meta_key' => Document::BUILT_WITH_ELEMENTOR_META_KEY,
'sort_order' => 'asc',
'sort_column' => 'post_date',
'number' => 1,
] )[0] ?? null;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Core\DocumentTypes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Post extends PageBase {
/**
* Get Properties
*
* Return the post document configuration properties.
*
* @access public
* @static
*
* @return array
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
$properties['cpt'] = [ 'post' ];
return $properties;
}
/**
* Get Type
*
* Return the post document type.
*
* @return string
*/
public static function get_type() {
return 'wp-post';
}
/**
* Get Title
*
* Return the post document title.
*
* @access public
* @static
*
* @return string
*/
public static function get_title() {
return esc_html__( 'Post', 'elementor' );
}
/**
* Get Plural Title
*
* Return the post document plural title.
*
* @access public
* @static
*
* @return string
*/
public static function get_plural_title() {
return esc_html__( 'Posts', 'elementor' );
}
}

View File

@@ -0,0 +1,821 @@
<?php
namespace Elementor\Core;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\DocumentTypes\Page;
use Elementor\Core\DocumentTypes\Post;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor documents manager.
*
* Elementor documents manager handler class is responsible for registering and
* managing Elementor documents.
*
* @since 2.0.0
*/
class Documents_Manager {
/**
* Registered types.
*
* Holds the list of all the registered types.
*
* @since 2.0.0
* @access protected
*
* @var Document[]
*/
protected $types = [];
/**
* Registered documents.
*
* Holds the list of all the registered documents.
*
* @since 2.0.0
* @access protected
*
* @var Document[]
*/
protected $documents = [];
/**
* Current document.
*
* Holds the current document.
*
* @since 2.0.0
* @access protected
*
* @var Document
*/
protected $current_doc;
/**
* Switched data.
*
* Holds the current document when changing to the requested post.
*
* @since 2.0.0
* @access protected
*
* @var array
*/
protected $switched_data = [];
protected $cpt = [];
/**
* Documents manager constructor.
*
* Initializing the Elementor documents manager.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
add_action( 'elementor/documents/register', [ $this, 'register_default_types' ], 0 );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
add_filter( 'post_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
add_filter( 'page_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
add_filter( 'user_has_cap', [ $this, 'remove_user_edit_cap' ], 10, 3 );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
}
/**
* Register ajax actions.
*
* Process ajax action handles when saving data and discarding changes.
*
* Fired by `elementor/ajax/register_actions` action.
*
* @since 2.0.0
* @access public
*
* @param Ajax $ajax_manager An instance of the ajax manager.
*/
public function register_ajax_actions( $ajax_manager ) {
$ajax_manager->register_ajax_action( 'save_builder', [ $this, 'ajax_save' ] );
$ajax_manager->register_ajax_action( 'discard_changes', [ $this, 'ajax_discard_changes' ] );
$ajax_manager->register_ajax_action( 'get_document_config', [ $this, 'ajax_get_document_config' ] );
}
/**
* Register default types.
*
* Registers the default document types.
*
* @since 2.0.0
* @access public
*/
public function register_default_types() {
$default_types = [
'post' => Post::get_class_full_name(), // BC.
'wp-post' => Post::get_class_full_name(),
'wp-page' => Page::get_class_full_name(),
];
foreach ( $default_types as $type => $class ) {
$this->register_document_type( $type, $class );
}
}
/**
* Register document type.
*
* Registers a single document.
*
* @since 2.0.0
* @access public
*
* @param string $type Document type name.
* @param string $class_name The name of the class that registers the document type.
* Full name with the namespace.
*
* @return Documents_Manager The updated document manager instance.
*/
public function register_document_type( $type, $class_name ) {
$this->types[ $type ] = $class_name;
$cpt = $class_name::get_property( 'cpt' );
if ( $cpt ) {
foreach ( $cpt as $post_type ) {
$this->cpt[ $post_type ] = $type;
}
}
if ( $class_name::get_property( 'register_type' ) ) {
Source_Local::add_template_type( $type );
}
return $this;
}
/**
* Get document.
*
* Retrieve the document data based on a post ID.
*
* @since 2.0.0
* @access public
*
* @param int $post_id Post ID.
* @param bool $from_cache Optional. Whether to retrieve cached data. Default is true.
*
* @return false|Document Document data or false if post ID was not entered.
*/
public function get( $post_id, $from_cache = true ) {
$this->register_types();
$post_id = absint( $post_id );
if ( ! $post_id || ! get_post( $post_id ) ) {
return false;
}
/**
* Retrieve document post ID.
*
* Filters the document post ID.
*
* @since 2.0.7
*
* @param int $post_id The post ID of the document.
*/
$post_id = apply_filters( 'elementor/documents/get/post_id', $post_id );
if ( ! $from_cache || ! isset( $this->documents[ $post_id ] ) ) {
$doc_type = $this->get_doc_type_by_id( $post_id );
$doc_type_class = $this->get_document_type( $doc_type );
$this->documents[ $post_id ] = new $doc_type_class( [
'post_id' => $post_id,
] );
}
return $this->documents[ $post_id ];
}
/**
* Retrieve a document after checking it exist and allowed to edit.
*
* @param string $id
* @return Document
* @throws \Exception If the document is not found or the current user is not allowed to edit it.
* @since 3.13.0
*/
public function get_with_permissions( $id ): Document {
$document = $this->get( $id );
if ( ! $document ) {
throw new \Exception( 'Not found.' );
}
if ( ! $document->is_editable_by_current_user() ) {
throw new \Exception( 'Access denied.' );
}
return $document;
}
/**
* A `void` version for `get_with_permissions`.
*
* @param string $id
* @return void
* @throws \Exception If the document is not found or the current user is not allowed to edit it.
*/
public function check_permissions( $id ) {
$this->get_with_permissions( $id );
}
/**
* Get document or autosave.
*
* Retrieve either the document or the autosave.
*
* @since 2.0.0
* @access public
*
* @param int $id Optional. Post ID. Default is `0`.
* @param int $user_id Optional. User ID. Default is `0`.
*
* @return false|Document The document if it exist, False otherwise.
*/
public function get_doc_or_auto_save( $id, $user_id = 0 ) {
$document = $this->get( $id );
if ( $document && $document->get_autosave_id( $user_id ) ) {
$document = $document->get_autosave( $user_id );
}
return $document;
}
/**
* Get document for frontend.
*
* Retrieve the document for frontend use.
*
* @since 2.0.0
* @access public
*
* @param int $post_id Optional. Post ID. Default is `0`.
*
* @return false|Document The document if it exist, False otherwise.
*/
public function get_doc_for_frontend( $post_id ) {
$preview_id = (int) Utils::get_super_global_value( $_GET, 'preview_id' );
$is_preview = is_preview();
$is_nonce_verify = wp_verify_nonce( Utils::get_super_global_value( $_GET, 'preview_nonce' ), 'post_preview_' . $preview_id );
if ( ( $is_preview && $is_nonce_verify ) || Plugin::$instance->preview->is_preview_mode() ) {
$document = $this->get_doc_or_auto_save( $post_id, get_current_user_id() );
} else {
$document = $this->get( $post_id );
}
return $document;
}
/**
* Get document type.
*
* Retrieve the type of any given document.
*
* @since 2.0.0
* @access public
*
* @param string $type
*
* @param string $fallback
*
* @return Document|bool The type of the document.
*/
public function get_document_type( $type, $fallback = 'post' ) {
$types = $this->get_document_types();
if ( isset( $types[ $type ] ) ) {
return $types[ $type ];
}
if ( isset( $types[ $fallback ] ) ) {
return $types[ $fallback ];
}
return false;
}
/**
* Get document types.
*
* Retrieve the all the registered document types.
*
* @since 2.0.0
* @access public
*
* @param array $args Optional. An array of key => value arguments to match against
* the properties. Default is empty array.
* @param string $operator Optional. The logical operation to perform. 'or' means only one
* element from the array needs to match; 'and' means all elements
* must match; 'not' means no elements may match. Default 'and'.
*
* @return Document[] All the registered document types.
*/
public function get_document_types( $args = [], $operator = 'and' ) {
$this->register_types();
if ( ! empty( $args ) ) {
$types_properties = $this->get_types_properties();
$filtered = wp_filter_object_list( $types_properties, $args, $operator );
return array_intersect_key( $this->types, $filtered );
}
return $this->types;
}
/**
* Get document types with their properties.
*
* @return array A list of properties arrays indexed by the type.
*/
public function get_types_properties() {
$types_properties = [];
foreach ( $this->get_document_types() as $type => $class ) {
$types_properties[ $type ] = $class::get_properties();
}
return $types_properties;
}
/**
* Create a document.
*
* Create a new document using any given parameters.
*
* @since 2.0.0
* @access public
*
* @param string $type Document type.
* @param array $post_data An array containing the post data.
* @param array $meta_data An array containing the post meta data.
*
* @return Document The type of the document.
*/
public function create( $type, $post_data = [], $meta_data = [] ) {
$class = $this->get_document_type( $type, false );
if ( ! $class ) {
return new \WP_Error( 500, sprintf( 'Type %s does not exist.', $type ) );
}
if ( empty( $post_data['post_title'] ) ) {
$post_data['post_title'] = esc_html__( 'Elementor', 'elementor' );
if ( 'post' !== $type ) {
$post_data['post_title'] = sprintf(
/* translators: %s: Document title. */
__( 'Elementor %s', 'elementor' ),
call_user_func( [ $class, 'get_title' ] )
);
}
$update_title = true;
}
$meta_data['_elementor_edit_mode'] = 'builder';
// Save the type as-is for plugins that hooked at `wp_insert_post`.
$meta_data[ Document::TYPE_META_KEY ] = $type;
$post_data['meta_input'] = $meta_data;
$post_types = $class::get_property( 'cpt' );
if ( ! empty( $post_types[0] ) && empty( $post_data['post_type'] ) ) {
$post_data['post_type'] = $post_types[0];
}
$post_id = wp_insert_post( $post_data );
if ( ! empty( $update_title ) ) {
$post_data['ID'] = $post_id;
$post_data['post_title'] .= ' #' . $post_id;
// The meta doesn't need update.
unset( $post_data['meta_input'] );
wp_update_post( $post_data );
}
/** @var Document $document */
$document = new $class( [
'post_id' => $post_id,
] );
// Let the $document to re-save the template type by his way + version.
$document->save( [] );
return $document;
}
/**
* Remove user edit capabilities if document is not editable.
*
* Filters the user capabilities to disable editing in admin.
*
* @param array $allcaps An array of all the user's capabilities.
* @param array $caps Actual capabilities for meta capability.
* @param array $args Optional parameters passed to has_cap(), typically object ID.
*
* @return array
*/
public function remove_user_edit_cap( $allcaps, $caps, $args ) {
global $pagenow;
if ( ! in_array( $pagenow, [ 'post.php', 'edit.php' ], true ) ) {
return $allcaps;
}
// Don't touch not existing or not allowed caps.
if ( empty( $caps[0] ) || empty( $allcaps[ $caps[0] ] ) ) {
return $allcaps;
}
$capability = $args[0];
if ( 'edit_post' !== $capability ) {
return $allcaps;
}
if ( empty( $args[2] ) ) {
return $allcaps;
}
$post_id = $args[2];
$document = Plugin::$instance->documents->get( $post_id );
if ( ! $document ) {
return $allcaps;
}
$allcaps[ $caps[0] ] = $document::get_property( 'is_editable' );
return $allcaps;
}
/**
* Filter Post Row Actions.
*
* Let the Document to filter the array of row action links on the Posts list table.
*
* @param array $actions
* @param \WP_Post $post
*
* @return array
*/
public function filter_post_row_actions( $actions, $post ) {
$document = $this->get( $post->ID );
if ( $document ) {
$actions = $document->filter_admin_row_actions( $actions );
}
return $actions;
}
/**
* Save document data using ajax.
*
* Save the document on the builder using ajax, when saving the changes, and refresh the editor.
*
* @since 2.0.0
* @access public
*
* @param array $request Post ID.
*
* @throws \Exception If current user don't have permissions to edit the post or the post is not using Elementor.
*
* @return array The document data after saving.
*/
public function ajax_save( $request ) {
$document = $this->get( $request['editor_post_id'] );
if ( ! $document->is_built_with_elementor() || ! $document->is_editable_by_current_user() ) {
throw new \Exception( 'Access denied.' );
}
$this->switch_to_document( $document );
// Set the post as global post.
Plugin::$instance->db->switch_to_post( $document->get_post()->ID );
$status = Document::STATUS_DRAFT;
if ( isset( $request['status'] ) && in_array( $request['status'], [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE, Document::STATUS_PENDING, Document::STATUS_AUTOSAVE ], true ) ) {
$status = $request['status'];
}
if ( Document::STATUS_AUTOSAVE === $status ) {
// If the post is a draft - save the `autosave` to the original draft.
// Allow a revision only if the original post is already published.
if ( in_array( $document->get_post()->post_status, [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE ], true ) ) {
$document = $document->get_autosave( 0, true );
}
}
// Set default page template because the footer-saver doesn't send default values,
// But if the template was changed from canvas to default - it needed to save.
if ( Utils::is_cpt_custom_templates_supported() && ! isset( $request['settings']['template'] ) ) {
$request['settings']['template'] = 'default';
}
$data = [
'elements' => $request['elements'],
'settings' => $request['settings'],
];
$document->save( $data );
$post = $document->get_post();
$main_post = $document->get_main_post();
// Refresh after save.
$document = $this->get( $post->ID, false );
$return_data = [
'status' => $post->post_status,
'config' => [
'document' => [
'last_edited' => $document->get_last_edited(),
'urls' => [
'wp_preview' => $document->get_wp_preview_url(),
],
],
],
];
$post_status_object = get_post_status_object( $main_post->post_status );
if ( $post_status_object ) {
$return_data['config']['document']['status'] = [
'value' => $post_status_object->name,
'label' => $post_status_object->label,
];
}
/**
* Returned documents ajax saved data.
*
* Filters the ajax data returned when saving the post on the builder.
*
* @since 2.0.0
*
* @param array $return_data The returned data.
* @param Document $document The document instance.
*/
$return_data = apply_filters( 'elementor/documents/ajax_save/return_data', $return_data, $document );
return $return_data;
}
/**
* Ajax discard changes.
*
* Load the document data from an autosave, deleting unsaved changes.
*
* @param array $request
*
* @return bool True if changes discarded, False otherwise.
* @throws \Exception If current user don't have permissions to edit the post or the post is not using Elementor.
*
* @since 2.0.0
* @access public
*/
public function ajax_discard_changes( $request ) {
$document = $this->get_with_permissions( $request['editor_post_id'] );
$autosave = $document->get_autosave();
if ( $autosave ) {
$success = $autosave->delete();
} else {
$success = true;
}
return $success;
}
public function ajax_get_document_config( $request ) {
$post_id = absint( $request['id'] );
Plugin::$instance->editor->set_post_id( $post_id );
$document = $this->get_doc_or_auto_save( $post_id );
if ( ! $document ) {
throw new \Exception( 'Not found.' );
}
if ( ! $document->is_editable_by_current_user() ) {
throw new \Exception( 'Access denied.' );
}
// Set the global data like $post, $authordata and etc
Plugin::$instance->db->switch_to_post( $post_id );
$this->switch_to_document( $document );
// Change mode to Builder
$document->set_is_built_with_elementor( true );
$doc_config = $document->get_config();
return $doc_config;
}
/**
* Switch to document.
*
* Change the document to any new given document type.
*
* @since 2.0.0
* @access public
*
* @param Document $document The document to switch to.
*/
public function switch_to_document( $document ) {
// If is already switched, or is the same post, return.
if ( $this->current_doc === $document ) {
$this->switched_data[] = false;
return;
}
$this->switched_data[] = [
'switched_doc' => $document,
'original_doc' => $this->current_doc, // Note, it can be null if the global isn't set
];
$this->current_doc = $document;
}
/**
* Restore document.
*
* Rollback to the original document.
*
* @since 2.0.0
* @access public
*/
public function restore_document() {
$data = array_pop( $this->switched_data );
// If not switched, return.
if ( ! $data ) {
return;
}
$this->current_doc = $data['original_doc'];
}
/**
* Get current document.
*
* Retrieve the current document.
*
* @since 2.0.0
* @access public
*
* @return Document The current document.
*/
public function get_current() {
return $this->current_doc;
}
public function localize_settings( $settings ) {
$translations = [];
foreach ( $this->get_document_types() as $type => $class ) {
$translations[ $type ] = $class::get_title();
}
return array_replace_recursive( $settings, [
'i18n' => $translations,
] );
}
private function register_types() {
if ( ! did_action( 'elementor/documents/register' ) ) {
/**
* Register Elementor documents.
*
* @since 2.0.0
*
* @param Documents_Manager $this The document manager instance.
*/
do_action( 'elementor/documents/register', $this );
}
}
/**
* Get create new post URL.
*
* Retrieve a custom URL for creating a new post/page using Elementor.
*
* @param string $post_type Optional. Post type slug. Default is 'page'.
* @param string|null $template_type Optional. Query arg 'template_type'. Default is null.
*
* @return string A URL for creating new post using Elementor.
*/
public static function get_create_new_post_url( $post_type = 'page', $template_type = null ) {
$query_args = [
'action' => 'elementor_new_post',
'post_type' => $post_type,
];
if ( $template_type ) {
$query_args['template_type'] = $template_type;
}
$new_post_url = add_query_arg( $query_args, admin_url( 'edit.php' ) );
$new_post_url = add_query_arg( '_wpnonce', wp_create_nonce( 'elementor_action_new_post' ), $new_post_url );
return $new_post_url;
}
private function get_doc_type_by_id( $post_id ) {
// Auto-save inherits from the original post.
if ( wp_is_post_autosave( $post_id ) ) {
$post_id = wp_get_post_parent_id( $post_id );
}
// Content built with Elementor.
$template_type = get_post_meta( $post_id, Document::TYPE_META_KEY, true );
if ( $template_type && isset( $this->types[ $template_type ] ) ) {
return $template_type;
}
// Elementor installation on a site with existing content (which doesn't contain Elementor's meta).
$post_type = get_post_type( $post_id );
return $this->cpt[ $post_type ] ?? 'post';
}
public function register_rest_routes() {
register_rest_route('elementor/v1/documents', '/(?P<id>\d+)/media/import', [
'methods' => \WP_REST_Server::CREATABLE,
'callback' => function( $request ) {
$post_id = $request->get_param( 'id' );
try {
$document = $this->get_with_permissions( $post_id );
$elements_data = $document->get_elements_data();
$import_data = $document->get_import_data( [
'content' => $elements_data,
] );
$document->save( [
'elements' => $import_data['content'],
] );
return new \WP_REST_Response( [
'success' => true,
'document_saved' => true,
], 200 );
} catch ( \Exception $e ) {
return new \WP_Error(
'elementor_import_error',
$e->getMessage(),
[ 'status' => 500 ]
);
}
},
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'args' => [
'id' => [
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
],
],
]);
}
}

View File

@@ -0,0 +1,196 @@
<?php
namespace Elementor\Core\DynamicTags;
use Elementor\Controls_Stack;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor base tag.
*
* An abstract class to register new Elementor tags.
*
* @since 2.0.0
* @abstract
*/
abstract class Base_Tag extends Controls_Stack {
/**
* @since 2.0.0
* @access public
* @static
*/
final public static function get_type() {
return 'tag';
}
/**
* @since 2.0.0
* @access public
* @abstract
*/
abstract public function get_categories();
/**
* @since 2.0.0
* @access public
* @abstract
*/
abstract public function get_group();
public function get_atomic_group() {
return $this->get_group();
}
/**
* @since 2.0.0
* @access public
* @abstract
*/
abstract public function get_title();
/**
* @since 2.0.0
* @access public
* @abstract
*
* @param array $options
*/
abstract public function get_content( array $options = [] );
/**
* @since 2.0.0
* @access public
* @abstract
*/
abstract public function get_content_type();
/**
* @since 2.0.0
* @access public
*/
public function get_panel_template_setting_key() {
return '';
}
/**
* @since 2.0.0
* @access public
*/
public function is_settings_required() {
return false;
}
/**
* @since 2.0.9
* @access public
*/
public function get_editor_config() {
ob_start();
$this->print_panel_template();
$panel_template = ob_get_clean();
return [
'name' => $this->get_name(),
'title' => $this->get_title(),
'panel_template' => $panel_template,
'categories' => $this->get_categories(),
'group' => $this->get_group(),
'atomic_group' => $this->get_atomic_group(),
'controls' => $this->get_controls(),
'content_type' => $this->get_content_type(),
'settings_required' => $this->is_settings_required(),
'editable' => $this->is_editable(),
];
}
/**
* @since 2.0.0
* @access public
*/
public function print_panel_template() {
$panel_template_setting_key = $this->get_panel_template_setting_key();
if ( ! $panel_template_setting_key ) {
return;
}
?><#
var key = <?php echo esc_html( $panel_template_setting_key ); ?>;
if ( key ) {
var settingsKey = "<?php echo esc_html( $panel_template_setting_key ); ?>";
/*
* If the tag has controls,
* and key is an existing control (and not an old one),
* and the control has options (select/select2),
* and the key is an existing option (and not in a group or an old one).
*/
if ( controls && controls[settingsKey] ) {
var controlSettings = controls[settingsKey];
if ( controlSettings.options && controlSettings.options[ key ] ) {
key = controlSettings.options[ key ];
} else if ( controlSettings.groups ) {
var label = _.filter( _.pluck( _.pluck( controls.key.groups, 'options' ), key ) );
if ( label[0] ) {
key = label[0];
}
}
}
print( '(' + _.escape( key ) + ')' );
}
#>
<?php
}
/**
* @since 2.0.0
* @access public
*/
final public function get_unique_name() {
return 'tag-' . $this->get_name();
}
/**
* @since 2.0.0
* @access protected
*/
protected function register_advanced_section() {}
/**
* @since 2.0.0
* @access protected
*/
final protected function init_controls() {
Plugin::$instance->controls_manager->open_stack( $this );
$this->start_controls_section( 'settings', [
'label' => esc_html__( 'Settings', 'elementor' ),
] );
if ( $this->has_own_method( '_register_controls' ) ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( '_register_controls', '3.1.0', __CLASS__ . '::register_controls()' );
$this->_register_controls();
} else {
$this->register_controls();
}
$this->end_controls_section();
// If in fact no controls were registered, empty the stack
if ( 1 === count( Plugin::$instance->controls_manager->get_stacks( $this->get_unique_name() )['controls'] ) ) {
Plugin::$instance->controls_manager->open_stack( $this );
}
$this->register_advanced_section();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Elementor\Core\DynamicTags;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor base data tag.
*
* An abstract class to register new Elementor data tags.
*
* @since 2.0.0
* @abstract
*/
abstract class Data_Tag extends Base_Tag {
/**
* @since 2.0.0
* @access protected
* @abstract
*
* @param array $options
*/
abstract protected function get_value( array $options = [] );
/**
* @since 2.0.0
* @access public
*/
final public function get_content_type() {
return 'plain';
}
/**
* @since 2.0.0
* @access public
*
* @param array $options
*
* @return mixed
*/
public function get_content( array $options = [] ) {
return $this->get_value( $options );
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Elementor\Core\DynamicTags;
use Elementor\Controls_Stack;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Core\Files\CSS\Post_Local_Cache;
use Elementor\Core\Files\CSS\Post_Preview;
use Elementor\Element_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Dynamic_CSS extends Post_Local_Cache {
private $post_dynamic_elements_ids;
private $post_id_for_data;
protected function get_post_id_for_data() {
if ( empty( $this->post_dynamic_elements_ids ) ) {
return null;
}
return $this->post_id_for_data;
}
protected function is_global_parsing_supported() {
return false;
}
protected function render_styles( Element_Base $element ) {
$id = $element->get_id();
if ( in_array( $id, $this->post_dynamic_elements_ids ) ) {
parent::render_styles( $element );
}
foreach ( $element->get_children() as $child_element ) {
$this->render_styles( $child_element );
}
}
/**
* Dynamic_CSS constructor.
*
* @since 2.0.13
* @access public
*
* @param int $post_id Post ID.
* @param Post_CSS $post_css_file
*/
public function __construct( $post_id, Post_CSS $post_css_file ) {
if ( $post_css_file instanceof Post_Preview ) {
$this->post_id_for_data = $post_css_file->get_post_id_for_data();
} else {
$this->post_id_for_data = $post_id;
}
$this->post_dynamic_elements_ids = $post_css_file->get_meta( 'dynamic_elements_ids' );
parent::__construct( $post_id );
}
/**
* @since 2.0.13
* @access public
*/
public function get_name() {
return 'dynamic';
}
/**
* Get Responsive Control Duplication Mode
*
* @since 3.4.0
*
* @return string
*/
protected function get_responsive_control_duplication_mode() {
return 'dynamic';
}
/**
* @since 2.0.13
* @access protected
*/
protected function use_external_file() {
return false;
}
/**
* @since 2.0.13
* @access protected
*/
protected function get_file_handle_id() {
return 'elementor-post-dynamic-' . $this->get_post_id_for_data();
}
/**
* @since 2.0.13
* @access public
*/
public function add_controls_stack_style_rules( Controls_Stack $controls_stack, array $controls, array $values, array $placeholders, array $replacements, ?array $all_controls = null ) {
$dynamic_settings = $controls_stack->get_settings( '__dynamic__' );
if ( ! empty( $dynamic_settings ) ) {
$controls = array_intersect_key( $controls, $dynamic_settings );
$all_controls = $controls_stack->get_controls();
$parsed_dynamic_settings = $controls_stack->parse_dynamic_settings( $values, $controls );
foreach ( $controls as $control ) {
if ( ! empty( $control['style_fields'] ) ) {
$this->add_repeater_control_style_rules( $controls_stack, $control, $values[ $control['name'] ], $placeholders, $replacements );
}
if ( empty( $control['selectors'] ) ) {
continue;
}
$this->add_control_style_rules( $control, $parsed_dynamic_settings, $all_controls, $placeholders, $replacements );
}
}
}
}

View File

@@ -0,0 +1,537 @@
<?php
namespace Elementor\Core\DynamicTags;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\CSS\Post;
use Elementor\Core\Files\CSS\Post_Preview;
use Elementor\Plugin;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Manager {
const TAG_LABEL = 'elementor-tag';
const MODE_RENDER = 'render';
const MODE_REMOVE = 'remove';
const DYNAMIC_SETTING_KEY = '__dynamic__';
const CONTROL_OPTION_KEYS = [ 'id', 'label' ];
private $tags_groups = [];
private $tags_info = [];
private $parsing_mode = self::MODE_RENDER;
/**
* Dynamic tags manager constructor.
*
* Initializing Elementor dynamic tags manager.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
$this->add_actions();
}
/**
* Parse dynamic tags text.
*
* Receives the dynamic tag text, and returns a single value or multiple values
* from the tag callback function.
*
* @since 2.0.0
* @access public
*
* @param string $text Dynamic tag text.
* @param array $settings The dynamic tag settings.
* @param callable $parse_callback The functions that renders the dynamic tag.
*
* @return string|string[]|mixed A single string or an array of strings with
* the return values from each tag callback
* function.
*/
public function parse_tags_text( $text, array $settings, callable $parse_callback ) {
if ( ! empty( $settings['returnType'] ) && 'object' === $settings['returnType'] ) {
$value = $this->parse_tag_text( $text, $settings, $parse_callback );
} else {
$value = preg_replace_callback( '/\[' . self::TAG_LABEL . '.+?(?=\])\]/', function( $tag_text_match ) use ( $settings, $parse_callback ) {
return $this->parse_tag_text( $tag_text_match[0], $settings, $parse_callback );
}, $text );
}
return $value;
}
/**
* Parse dynamic tag text.
*
* Receives the dynamic tag text, and returns the value from the callback
* function.
*
* @since 2.0.0
* @access public
*
* @param string $tag_text Dynamic tag text.
* @param array $settings The dynamic tag settings.
* @param callable $parse_callback The functions that renders the dynamic tag.
*
* @return string|array|mixed If the tag was not found an empty string or an
* empty array will be returned, otherwise the
* return value from the tag callback function.
*/
public function parse_tag_text( $tag_text, array $settings, callable $parse_callback ) {
$tag_data = $this->tag_text_to_tag_data( $tag_text );
if ( ! $tag_data ) {
if ( ! empty( $settings['returnType'] ) && 'object' === $settings['returnType'] ) {
return [];
}
return '';
}
return call_user_func_array( $parse_callback, array_values( $tag_data ) );
}
/**
* @since 2.0.0
* @access public
*
* @param string $tag_text
*
* @return array|null
*/
public function tag_text_to_tag_data( $tag_text ) {
preg_match( '/id="(.*?(?="))"/', $tag_text, $tag_id_match );
preg_match( '/name="(.*?(?="))"/', $tag_text, $tag_name_match );
preg_match( '/settings="(.*?(?="]))/', $tag_text, $tag_settings_match );
if ( ! $tag_id_match || ! $tag_name_match || ! $tag_settings_match ) {
return null;
}
return [
'id' => $tag_id_match[1],
'name' => $tag_name_match[1],
'settings' => json_decode( urldecode( $tag_settings_match[1] ), true ),
];
}
/**
* Dynamic tag to text.
*
* Retrieve the shortcode that represents the dynamic tag.
*
* @since 2.0.0
* @access public
*
* @param Base_Tag $tag An instance of the dynamic tag.
*
* @return string The shortcode that represents the dynamic tag.
*/
public function tag_to_text( Base_Tag $tag ) {
return sprintf( '[%1$s id="%2$s" name="%3$s" settings="%4$s"]', self::TAG_LABEL, $tag->get_id(), $tag->get_name(), urlencode( wp_json_encode( $tag->get_settings(), JSON_FORCE_OBJECT ) ) );
}
/**
* @since 2.0.0
* @access public
* @param string $tag_id
* @param string $tag_name
* @param array $settings
*
* @return string
*/
public function tag_data_to_tag_text( $tag_id, $tag_name, array $settings = [] ) {
$tag = $this->create_tag( $tag_id, $tag_name, $settings );
if ( ! $tag ) {
return '';
}
return $this->tag_to_text( $tag );
}
private function normalize_settings( $value ) {
if ( $this->is_typed_value_wrapper( $value ) ) {
return $this->normalize_settings( $value['value'] );
}
if ( $this->is_id_label_option( $value ) ) {
return $this->normalize_settings( $value['id'] );
}
if ( is_array( $value ) ) {
foreach ( $value as $k => $v ) {
$value[ $k ] = $this->normalize_settings( $v );
}
return $value;
}
if ( is_object( $value ) ) {
return $this->normalize_settings( get_object_vars( $value ) );
}
return $value;
}
private function is_typed_value_wrapper( $value ) {
return is_array( $value ) && isset( $value['$$type'], $value['value'] );
}
private function is_id_label_option( $value ) {
if ( ! is_array( $value ) || ! array_key_exists( 'id', $value ) ) {
return false;
}
$keys = array_keys( $value );
return empty( array_diff( $keys, self::CONTROL_OPTION_KEYS ) );
}
/**
* @since 2.0.0
* @access public
* @param string $tag_id
* @param string $tag_name
* @param array $settings
*
* @return Tag|null
*/
public function create_tag( $tag_id, $tag_name, array $settings = [] ) {
$tag_info = $this->get_tag_info( $tag_name );
if ( ! $tag_info ) {
return null;
}
$tag_class = $tag_info['class'];
return new $tag_class( [
'settings' => $this->normalize_settings( $settings ),
'id' => $tag_id,
] );
}
/**
* @since 2.0.0
* @access public
*
* @param $tag_id
* @param $tag_name
* @param array $settings
*
* @return null|string
*/
public function get_tag_data_content( $tag_id, $tag_name, array $settings = [] ) {
if ( self::MODE_REMOVE === $this->parsing_mode ) {
return null;
}
$tag = $this->create_tag( $tag_id, $tag_name, $this->normalize_settings( $settings ) );
if ( ! $tag ) {
return null;
}
return $tag->get_content();
}
/**
* @since 2.0.0
* @access public
*
* @param $tag_name
*
* @return mixed|null
*/
public function get_tag_info( $tag_name ) {
$tags = $this->get_tags();
if ( empty( $tags[ $tag_name ] ) ) {
return null;
}
return $tags[ $tag_name ];
}
/**
* @since 2.0.9
* @access public
*/
public function get_tags() {
if ( ! did_action( 'elementor/dynamic_tags/register_tags' ) ) {
/**
* Register dynamic tags.
*
* Fires when Elementor registers dynamic tags.
*
* @since 2.0.9
* @deprecated 3.5.0 Use `elementor/dynamic_tags/register` hook instead.
*
* @param Manager $this Dynamic tags manager.
*/
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->do_deprecated_action(
'elementor/dynamic_tags/register_tags',
[ $this ],
'3.5.0',
'elementor/dynamic_tags/register'
);
}
if ( ! did_action( 'elementor/dynamic_tags/register' ) ) {
/**
* Register dynamic tags.
*
* Fires when Elementor registers dynamic tags.
*
* @since 3.5.0
*
* @param Manager $this Dynamic tags manager.
*/
do_action( 'elementor/dynamic_tags/register', $this );
}
return $this->tags_info;
}
/**
* @since 2.0.0
* @access public
* @deprecated 3.5.0 Use `register()` method instead.
*
* @param string $class_name
*/
public function register_tag( $class_name ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function(
__METHOD__,
'3.5.0',
'register()'
);
/** @var Base_Tag $tag */
$instance = new $class_name();
$this->register( $instance );
}
/**
* Register a new Dynamic Tag.
*
* @param Base_Tag $dynamic_tag_instance
*
* @return void
* @since 3.5.0
* @access public
*/
public function register( Base_Tag $dynamic_tag_instance ) {
$this->tags_info[ $dynamic_tag_instance->get_name() ] = [
'class' => get_class( $dynamic_tag_instance ),
'instance' => $dynamic_tag_instance,
];
}
/**
* @since 2.0.9
* @access public
* @deprecated 3.5.0 Use `unregister()` method instead.
*
* @param string $tag_name
*/
public function unregister_tag( $tag_name ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function(
__METHOD__,
'3.5.0',
'unregister()'
);
$this->unregister( $tag_name );
}
/**
* Unregister a dynamic tag.
*
* @since 3.5.0
* @access public
*
* @param string $tag_name Dynamic Tag name to unregister.
*
* @return void
*/
public function unregister( $tag_name ) {
unset( $this->tags_info[ $tag_name ] );
}
/**
* @since 2.0.0
* @access public
*
* @param $group_name
* @param array $group_settings
*/
public function register_group( $group_name, array $group_settings ) {
$default_group_settings = [
'title' => '',
];
$group_settings = array_merge( $default_group_settings, $group_settings );
$this->tags_groups[ $group_name ] = $group_settings;
}
/**
* @since 2.0.0
* @access public
*/
public function print_templates() {
foreach ( $this->get_tags() as $tag_name => $tag_info ) {
$tag = $tag_info['instance'];
if ( ! $tag instanceof Tag ) {
continue;
}
$tag->print_template();
}
}
/**
* @since 2.0.0
* @access public
*/
public function get_tags_config() {
$config = [];
foreach ( $this->get_tags() as $tag_name => $tag_info ) {
/** @var Tag $tag */
$tag = $tag_info['instance'];
$config[ $tag_name ] = $tag->get_editor_config();
}
return $config;
}
/**
* @since 2.0.0
* @access public
*/
public function get_config() {
return [
'tags' => $this->get_tags_config(),
'groups' => $this->tags_groups,
];
}
/**
* @since 2.0.0
* @access public
*
* @throws \Exception If post ID is missing or current user don't have permissions to edit the post.
*/
public function ajax_render_tags( $data ) {
if ( empty( $data['post_id'] ) ) {
throw new \Exception( 'Missing post id.' );
}
if ( ! User::is_current_user_can_edit( $data['post_id'] ) ) {
throw new \Exception( 'Access denied.' );
}
Plugin::$instance->db->switch_to_post( $data['post_id'] );
/**
* Before dynamic tags rendered.
*
* Fires before Elementor renders the dynamic tags.
*
* @since 2.0.0
*/
do_action( 'elementor/dynamic_tags/before_render' );
$tags_data = [];
foreach ( $data['tags'] as $tag_key ) {
$tag_key_parts = explode( '-', $tag_key );
$tag_name = base64_decode( $tag_key_parts[0] );
$tag_settings = json_decode( urldecode( base64_decode( $tag_key_parts[1] ) ), true );
$tag = $this->create_tag( null, $tag_name, $tag_settings );
$tags_data[ $tag_key ] = $tag->get_content();
}
/**
* After dynamic tags rendered.
*
* Fires after Elementor renders the dynamic tags.
*
* @since 2.0.0
*/
do_action( 'elementor/dynamic_tags/after_render' );
return $tags_data;
}
/**
* @since 2.0.0
* @access public
*
* @param $mode
*/
public function set_parsing_mode( $mode ) {
$this->parsing_mode = $mode;
}
/**
* @since 2.0.0
* @access public
*/
public function get_parsing_mode() {
return $this->parsing_mode;
}
/**
* @since 2.1.0
* @access public
* @param Post $css_file
*/
public function after_enqueue_post_css( $css_file ) {
$post_id = $css_file->get_post_id();
$should_enqueue = apply_filters( 'elementor/css-file/dynamic/should_enqueue', true, $post_id );
if ( $should_enqueue ) {
$css_file = Dynamic_CSS::create( $post_id, $css_file );
$css_file->enqueue();
}
}
/**
* @since 2.3.0
* @access public
*/
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'render_tags', [ $this, 'ajax_render_tags' ] );
}
/**
* @since 2.0.0
* @access private
*/
private function add_actions() {
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
add_action( 'elementor/css-file/post/enqueue', [ $this, 'after_enqueue_post_css' ] );
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Elementor\Core\DynamicTags;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor tag.
*
* An abstract class to register new Elementor tag.
*
* @since 2.0.0
* @abstract
*/
abstract class Tag extends Base_Tag {
const WRAPPED_TAG = false;
/**
* @since 2.0.0
* @access public
*
* @param array $options
*
* @return string
*/
public function get_content( array $options = [] ) {
$settings = $this->get_settings();
ob_start();
$this->render();
$value = ob_get_clean();
if ( ! Utils::is_empty( $value ) ) {
// TODO: fix spaces in `before`/`after` if WRAPPED_TAG ( conflicted with .elementor-tag { display: inline-flex; } );
if ( ! Utils::is_empty( $settings, 'before' ) ) {
$value = wp_kses_post( $settings['before'] ) . $value;
}
if ( ! Utils::is_empty( $settings, 'after' ) ) {
$value .= wp_kses_post( $settings['after'] );
}
if ( static::WRAPPED_TAG ) :
$value = '<span id="elementor-tag-' . esc_attr( $this->get_id() ) . '" class="elementor-tag">' . $value . '</span>';
endif;
} elseif ( ! Utils::is_empty( $settings, 'fallback' ) ) {
$value = wp_kses_post_deep( $settings['fallback'] );
}
return $value;
}
/**
* @since 2.0.0
* @access public
*/
final public function get_content_type() {
return 'ui';
}
/**
* @since 2.0.9
* @access public
*/
public function get_editor_config() {
$config = parent::get_editor_config();
$config['wrapped_tag'] = $this::WRAPPED_TAG;
return $config;
}
/**
* @since 2.0.0
* @access protected
*/
protected function register_advanced_section() {
$this->start_controls_section(
'advanced',
[
'label' => esc_html__( 'Advanced', 'elementor' ),
]
);
$this->add_control(
'before',
[
'label' => esc_html__( 'Before', 'elementor' ),
'ai' => [
'active' => false,
],
]
);
$this->add_control(
'after',
[
'label' => esc_html__( 'After', 'elementor' ),
'ai' => [
'active' => false,
],
]
);
$this->add_control(
'fallback',
[
'label' => esc_html__( 'Fallback', 'elementor' ),
'ai' => [
'active' => false,
],
]
);
$this->end_controls_section();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Elementor\Core\Editor\Data\Globals;
use Elementor\Data\V2\Base\Controller as Controller_Base;
use Elementor\Data\V2\Base\Endpoint;
use Elementor\Plugin;
class Controller extends Controller_Base {
public function get_name() {
return 'globals';
}
public function register_endpoints() {
$this->register_endpoint( new Endpoints\Colors( $this ) );
$this->register_endpoint( new Endpoints\Typography( $this ) );
}
public function get_collection_params() {
// Does not have 'get_items' args (OPTIONS).
// Maybe TODO: try `$this->get_index_endpoint()->get_collection_params()`.
return [];
}
public function get_permission_callback( $request ) {
// Allow internal get global values. (e.g render global.css for a visitor)
if ( 'GET' === $request->get_method() && Plugin::$instance->data_manager_v2->is_internal() ) {
return true;
}
return current_user_can( 'edit_posts' );
}
protected function register_index_endpoint() {
$this->register_endpoint( new Endpoint\Index\AllChildren( $this ) );
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Data\V2\Base\Endpoint;
use Elementor\Data\V2\Base\Exceptions\Data_Exception;
use Elementor\Data\V2\Base\Exceptions\Error_404;
use Elementor\Plugin;
abstract class Base extends Endpoint {
protected function register() {
parent::register();
$args = [
'id_arg_type_regex' => '[\w]+',
];
$this->register_item_route( \WP_REST_Server::READABLE, $args );
$this->register_item_route( \WP_REST_Server::CREATABLE, $args );
$this->register_item_route( \WP_REST_Server::DELETABLE, $args );
}
public function get_items( $request ) {
return $this->get_kit_items();
}
/**
* @inheritDoc
* @throws \Elementor\Data\V2\Base\Exceptions\Error_404 If the item is not found.
*/
public function get_item( $id, $request ) {
$items = $this->get_kit_items();
if ( ! isset( $items[ $id ] ) ) {
throw new Error_404( esc_html__( 'The Global value you are trying to use is not available.', 'elementor' ),
'global_not_found'
);
}
return $items[ $id ];
}
public function create_item( $id, $request ) {
$item = $request->get_body_params();
if ( ! isset( $item['title'] ) ) {
return new Data_Exception( esc_html__( 'Invalid title', 'elementor' ), 'invalid_title' );
}
$kit = Plugin::$instance->kits_manager->get_active_kit();
$item['id'] = $id;
$db_item = $this->convert_db_format( $item );
$kit->add_repeater_row( 'custom_' . $this->get_name(), $db_item );
return $item;
}
abstract protected function get_kit_items();
/**
* @param array $item frontend format.
* @return array backend format.
*/
abstract protected function convert_db_format( $item );
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Plugin;
class Colors extends Base {
public function get_name() {
return 'colors';
}
public function get_format() {
return 'globals/colors/{id}';
}
protected function get_kit_items() {
$result = [];
$kit = Plugin::$instance->kits_manager->get_active_kit_for_frontend();
$system_items = $kit->get_settings_for_display( 'system_colors' );
$custom_items = $kit->get_settings_for_display( 'custom_colors' );
if ( ! $system_items ) {
$system_items = [];
}
if ( ! $custom_items ) {
$custom_items = [];
}
$items = array_merge( $system_items, $custom_items );
foreach ( $items as $index => $item ) {
$id = $item['_id'];
$result[ $id ] = [
'id' => $id,
'title' => $item['title'] ?? '',
'value' => $item['color'] ?? '',
];
}
return $result;
}
protected function convert_db_format( $item ) {
return [
'_id' => $item['id'],
'title' => sanitize_text_field( $item['title'] ?? '' ),
'color' => sanitize_text_field( $item['value'] ?? '' ),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Plugin;
class Typography extends Base {
public function get_name() {
return 'typography';
}
public function get_format() {
return 'globals/typography/{id}';
}
protected function get_kit_items() {
$result = [];
$kit = Plugin::$instance->kits_manager->get_active_kit_for_frontend();
// Use raw settings that doesn't have default values.
$kit_raw_settings = $kit->get_data( 'settings' );
if ( isset( $kit_raw_settings['system_typography'] ) ) {
$system_items = $kit_raw_settings['system_typography'];
} else {
// Get default items, but without empty defaults.
$control = $kit->get_controls( 'system_typography' );
$system_items = $control['default'];
}
$custom_items = $kit->get_settings( 'custom_typography' );
if ( ! $custom_items ) {
$custom_items = [];
}
$items = array_merge( $system_items, $custom_items );
foreach ( $items as $index => &$item ) {
foreach ( $item as $setting => $value ) {
$new_setting = str_replace( 'styles_', '', $setting, $count );
if ( $count ) {
$item[ $new_setting ] = $value;
unset( $item[ $setting ] );
}
}
$id = $item['_id'];
$result[ $id ] = [
'title' => $item['title'] ?? '',
'id' => $id,
];
unset( $item['_id'], $item['title'] );
$result[ $id ]['value'] = $item;
}
return $result;
}
protected function convert_db_format( $item ) {
$db_format = [
'_id' => $item['id'],
'title' => sanitize_text_field( $item['title'] ?? '' ),
];
$value = $item['value'];
unset( $value['_id'], $value['title'] );
foreach ( $value as $key => $dirty_value ) {
$db_format[ $key ] = is_string( $dirty_value ) ? sanitize_text_field( $dirty_value ) : $dirty_value ?? '';
}
return $db_format;
}
}

View File

@@ -0,0 +1,663 @@
<?php
namespace Elementor\Core\Editor;
use Elementor\Core\Breakpoints\Manager as Breakpoints_Manager;
use Elementor\Core\Common\Modules\Ajax\Module;
use Elementor\Core\Debug\Loading_Inspection_Manager;
use Elementor\Core\Editor\Loader\Editor_Loader_Factory;
use Elementor\Core\Editor\Loader\Editor_Loader_Interface;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
use Elementor\Core\Editor\Data;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor editor.
*
* Elementor editor handler class is responsible for initializing Elementor
* editor and register all the actions needed to display the editor.
*
* @since 1.0.0
*/
class Editor {
/**
* User capability required to access Elementor editor.
*/
const EDITING_CAPABILITY = 'edit_posts';
/**
* Post ID.
*
* Holds the ID of the current post being edited.
*
* @since 1.0.0
* @access private
*
* @var int Post ID.
*/
private $post_id;
/**
* Whether the edit mode is active.
*
* Used to determine whether we are in edit mode.
*
* @since 1.0.0
* @access private
*
* @var bool Whether the edit mode is active.
*/
private $is_edit_mode;
/**
* @var Notice_Bar
*/
public $notice_bar;
/**
* @var Promotion
*/
public $promotion;
/**
* @var Editor_Loader_Interface
*/
private $loader;
/**
* Init.
*
* Initialize Elementor editor. Registers all needed actions to run Elementor,
* removes conflicting actions etc.
*
* Fired by `admin_action_elementor` action.
*
* @since 1.0.0
* @access public
*
* @param bool $to_die Optional. Whether to die at the end. Default is `true`.
*/
public function init( $to_die = true ) {
if ( empty( $_REQUEST['post'] ) ) {
return;
}
$this->set_post_id( absint( $_REQUEST['post'] ) );
if ( ! $this->is_edit_mode( $this->post_id ) ) {
return;
}
// BC: From 2.9.0, the editor shouldn't handle the global post / current document.
// Use requested id and not the global in order to avoid conflicts with plugins that changes the global post.
query_posts( [
'p' => $this->post_id,
'post_type' => get_post_type( $this->post_id ),
] );
Plugin::$instance->db->switch_to_post( $this->post_id );
$document = Plugin::$instance->documents->get( $this->post_id );
Plugin::$instance->documents->switch_to_document( $document );
// Change mode to Builder
$document->set_is_built_with_elementor( true );
// End BC.
Loading_Inspection_Manager::instance()->register_inspections();
// Send MIME Type header like WP admin-header.
@header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
add_filter( 'show_admin_bar', '__return_false' );
// Remove all WordPress actions
remove_all_actions( 'wp_head' );
remove_all_actions( 'wp_print_styles' );
remove_all_actions( 'wp_print_head_scripts' );
remove_all_actions( 'wp_footer' );
// Handle `wp_head`
add_action( 'wp_head', 'wp_enqueue_scripts', 1 );
add_action( 'wp_head', 'wp_print_styles', 8 );
add_action( 'wp_head', 'wp_print_head_scripts', 9 );
add_action( 'wp_head', 'wp_site_icon' );
add_action( 'wp_head', [ $this, 'editor_head_trigger' ], 30 );
// Handle `wp_footer`
add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
add_action( 'wp_footer', 'wp_auth_check_html', 30 );
add_action( 'wp_footer', [ $this, 'wp_footer' ] );
// Handle `wp_enqueue_scripts`
remove_all_actions( 'wp_enqueue_scripts' );
// Also remove all scripts hooked into after_wp_tiny_mce.
remove_all_actions( 'after_wp_tiny_mce' );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ], 999999 );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ], 999999 );
// Setup default heartbeat options
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 15;
return $settings;
} );
// Tell to WP Cache plugins do not cache this request.
Utils::do_not_cache();
do_action( 'elementor/editor/init' );
$this->get_loader()->print_root_template();
// From the action it's an empty string, from tests its `false`
if ( false !== $to_die ) {
die;
}
}
/**
* Retrieve post ID.
*
* Get the ID of the current post.
*
* @since 1.8.0
* @access public
*
* @return int Post ID.
*/
public function get_post_id() {
return $this->post_id;
}
/**
* Redirect to new URL.
*
* Used as a fallback function for the old URL structure of Elementor page
* edit URL.
*
* Fired by `template_redirect` action.
*
* @since 1.6.0
* @access public
*/
public function redirect_to_new_url() {
if ( ! isset( $_GET['elementor'] ) ) {
return;
}
$document = Plugin::$instance->documents->get( get_the_ID() );
if ( ! $document ) {
wp_die( esc_html__( 'Document not found.', 'elementor' ) );
}
if ( ! $document->is_editable_by_current_user() || ! $document->is_built_with_elementor() ) {
return;
}
wp_safe_redirect( $document->get_edit_url() );
die;
}
/**
* Whether the edit mode is active.
*
* Used to determine whether we are in the edit mode.
*
* @since 1.0.0
* @access public
*
* @param int $post_id Optional. Post ID. Default is `null`, the current
* post ID.
*
* @return bool Whether the edit mode is active.
*/
public function is_edit_mode( $post_id = null ) {
if ( null !== $this->is_edit_mode ) {
return $this->is_edit_mode;
}
if ( empty( $post_id ) ) {
$post_id = $this->post_id;
}
$document = Plugin::$instance->documents->get( $post_id );
if ( ! $document || ! $document->is_editable_by_current_user() ) {
return false;
}
/** @var Module ajax */
$ajax_data = Plugin::$instance->common->get_component( 'ajax' )->get_current_action_data();
if ( ! empty( $ajax_data ) && 'get_document_config' === $ajax_data['action'] ) {
return true;
}
// Ajax request as Editor mode
$actions = [
'elementor',
// Templates
'elementor_get_templates',
'elementor_save_template',
'elementor_get_template',
'elementor_delete_template',
'elementor_import_template',
'elementor_library_direct_actions',
];
if ( isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], $actions ) ) {
return true;
}
return false;
}
/**
* Lock post.
*
* Mark the post as currently being edited by the current user.
*
* @since 1.0.0
* @access public
*
* @param int $post_id The ID of the post being edited.
*/
public function lock_post( $post_id ) {
if ( ! function_exists( 'wp_set_post_lock' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
wp_set_post_lock( $post_id );
}
/**
* Get locked user.
*
* Check what user is currently editing the post.
*
* @since 1.0.0
* @access public
*
* @param int $post_id The ID of the post being edited.
*
* @return \WP_User|false User information or false if the post is not locked.
*/
public function get_locked_user( $post_id ) {
if ( ! function_exists( 'wp_check_post_lock' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$locked_user = wp_check_post_lock( $post_id );
if ( ! $locked_user ) {
return false;
}
return get_user_by( 'id', $locked_user );
}
/**
* NOTICE: This method not in use, it's here for backward compatibility.
*
* Print Editor Template.
*
* Include the wrapper template of the editor.
*
* @since 2.2.0
* @access public
*/
public function print_editor_template() {
include ELEMENTOR_PATH . 'includes/editor-templates/editor-wrapper.php';
}
/**
* Enqueue scripts.
*
* Registers all the editor scripts and enqueues them.
*
* @since 1.0.0
* @access public
*/
public function enqueue_scripts() {
remove_action( 'wp_enqueue_scripts', [ $this, __FUNCTION__ ], 999999 );
global $wp_styles, $wp_scripts;
// Reset global variable
$wp_styles = new \WP_Styles(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_scripts = new \WP_Scripts(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$this->get_loader()->register_scripts();
/**
* Before editor enqueue scripts.
*
* Fires before Elementor editor scripts are enqueued.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/before_enqueue_scripts' );
// Tweak for WP Admin menu icons
wp_print_styles( 'editor-buttons' );
$this->get_loader()->enqueue_scripts();
Plugin::$instance->controls_manager->enqueue_control_scripts();
/**
* After editor enqueue scripts.
*
* Fires after Elementor editor scripts are enqueued.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/after_enqueue_scripts' );
}
/**
* Enqueue styles.
*
* Registers all the editor styles and enqueues them.
*
* @since 1.0.0
* @access public
*/
public function enqueue_styles() {
/**
* Before editor enqueue styles.
*
* Fires before Elementor editor styles are enqueued.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/before_enqueue_styles' );
$this->get_loader()->register_styles();
$this->get_loader()->enqueue_styles();
$this->enqueue_theme_ui_styles();
$breakpoints = Plugin::$instance->breakpoints->get_breakpoints();
// The two breakpoints under 'tablet' need to be checked for values.
if ( $breakpoints[ Breakpoints_Manager::BREAKPOINT_KEY_MOBILE ]->is_custom() || $breakpoints[ Breakpoints_Manager::BREAKPOINT_KEY_MOBILE_EXTRA ]->is_enabled() ) {
wp_add_inline_style(
'elementor-editor',
'.elementor-device-tablet #elementor-preview-responsive-wrapper { width: ' . Plugin::$instance->breakpoints->get_device_min_breakpoint( Breakpoints_Manager::BREAKPOINT_KEY_TABLET ) . 'px; }'
);
}
/**
* After editor enqueue styles.
*
* Fires after Elementor editor styles are enqueued.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/after_enqueue_styles' );
}
private function enqueue_theme_ui_styles() {
$ui_theme_selected = SettingsManager::get_settings_managers( 'editorPreferences' )->get_model()->get_settings( 'ui_theme' );
$ui_themes = [
'light',
'dark',
];
if ( 'auto' === $ui_theme_selected || ! in_array( $ui_theme_selected, $ui_themes, true ) ) {
$ui_light_theme_media_queries = '(prefers-color-scheme: light)';
$ui_dark_theme_media_queries = '(prefers-color-scheme: dark)';
} else {
$ui_light_theme_media_queries = 'none';
$ui_dark_theme_media_queries = 'none';
if ( 'light' === $ui_theme_selected ) {
$ui_light_theme_media_queries = 'all';
} elseif ( 'dark' === $ui_theme_selected ) {
$ui_dark_theme_media_queries = 'all';
}
}
$this->enqueue_theme_ui( 'light', $ui_light_theme_media_queries );
$this->enqueue_theme_ui( 'dark', $ui_dark_theme_media_queries );
}
private function enqueue_theme_ui( $ui_theme, $ui_theme_media_queries = 'all' ) {
$suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_style(
'e-theme-ui-' . $ui_theme,
ELEMENTOR_ASSETS_URL . 'css/theme-' . $ui_theme . $suffix . '.css',
[],
ELEMENTOR_VERSION,
$ui_theme_media_queries
);
}
/**
* Editor head trigger.
*
* Fires the 'elementor/editor/wp_head' action in the head tag in Elementor
* editor.
*
* @since 1.0.0
* @access public
*/
public function editor_head_trigger() {
/**
* Elementor editor head.
*
* Fires on Elementor editor head tag.
*
* Used to prints scripts or any other data in the head tag.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/wp_head' );
}
/**
* WP footer.
*
* Prints Elementor editor with all the editor templates, and render controls,
* widgets and content elements.
*
* Fired by `wp_footer` action.
*
* @since 1.0.0
* @access public
*/
public function wp_footer() {
$plugin = Plugin::$instance;
$plugin->controls_manager->render_controls();
$plugin->widgets_manager->render_widgets_content();
$plugin->elements_manager->render_elements_content();
$plugin->dynamic_tags->print_templates();
$this->get_loader()->register_additional_templates();
/**
* Elementor editor footer.
*
* Fires on Elementor editor before closing the body tag.
*
* Used to prints scripts or any other HTML before closing the body tag.
*
* @since 1.0.0
*/
do_action( 'elementor/editor/footer' );
}
/**
* Set edit mode.
*
* Used to update the edit mode.
*
* @since 1.0.0
* @access public
*
* @param bool $edit_mode Whether the edit mode is active.
*/
public function set_edit_mode( $edit_mode ) {
$this->is_edit_mode = $edit_mode;
}
/**
* Editor constructor.
*
* Initializing Elementor editor and redirect from old URL structure of
* Elementor editor.
*
* @since 1.0.0
* @access public
*/
public function __construct() {
Plugin::$instance->data_manager_v2->register_controller( new Data\Globals\Controller() );
$this->notice_bar = new Notice_Bar();
$this->promotion = new Promotion();
add_action( 'admin_action_elementor', [ $this, 'init' ] );
add_action( 'template_redirect', [ $this, 'redirect_to_new_url' ] );
// Handle autocomplete feature for URL control.
add_filter( 'wp_link_query_args', [ $this, 'filter_wp_link_query_args' ] );
add_filter( 'wp_link_query', [ $this, 'filter_wp_link_query' ] );
add_filter( 'replace_editor', [ $this, 'filter_replace_editor' ], 10, 2 );
}
/**
* Signals to WordPress that Elementor is replacing the block editor on its own editor page,
* so that block-editor-specific behaviour (e.g. WP 7.0 COOP/COEP isolation headers) is not
* applied when the Elementor editor is active.
*
* @param bool $replace Whether the editor is being replaced.
* @param \WP_Post $post The post being edited.
*
* @return bool
*/
public function filter_replace_editor( $replace, $post ) {
if ( isset( $_REQUEST['action'] ) && 'elementor' === $_REQUEST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return true;
}
return $replace;
}
/**
* @since 2.2.0
* @access public
*/
public function filter_wp_link_query_args( $query ) {
$library_cpt_key = array_search( Source_Local::CPT, $query['post_type'], true );
if ( false !== $library_cpt_key ) {
unset( $query['post_type'][ $library_cpt_key ] );
}
return $query;
}
/**
* @since 2.2.0
* @access public
*/
public function filter_wp_link_query( $results ) {
// PHPCS - The user data is not used.
if ( isset( $_POST['editor'] ) && 'elementor' === $_POST['editor'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$post_type_object = get_post_type_object( 'post' );
$post_label = $post_type_object->labels->singular_name;
foreach ( $results as & $result ) {
if ( 'post' === get_post_type( $result['ID'] ) ) {
$result['info'] = $post_label;
}
}
}
return $results;
}
public function set_post_id( $post_id ) {
$this->post_id = $post_id;
}
/**
* Get loader.
*
* @return Editor_Loader_Interface
*/
private function get_loader() {
if ( ! $this->loader ) {
$this->loader = Editor_Loader_Factory::create();
$this->loader->init();
}
return $this->loader;
}
/**
* Get elements presets.
*
* @return array
*/
public function get_elements_presets() {
$element_types = Plugin::$instance->elements_manager->get_element_types();
$presets = [];
foreach ( $element_types as $el_type => $element ) {
$this->check_element_for_presets( $element, $el_type, $presets );
}
return $presets;
}
/**
* @return void
*/
private function check_element_for_presets( $element, $el_type, &$presets ) {
$element_presets = $element->get_panel_presets();
if ( empty( $element_presets ) ) {
return;
}
foreach ( $element_presets as $key => $preset ) {
$this->maybe_add_preset( $el_type, $preset, $key, $presets );
}
}
/**
* @return void
*/
private function maybe_add_preset( $el_type, $preset, $key, &$presets ) {
if ( $this->is_valid_preset( $el_type, $preset ) ) {
$presets[ $key ] = $preset;
}
}
/**
* @return boolean
*/
private function is_valid_preset( $el_type, $preset ) {
return isset( $preset['replacements']['custom']['originalWidget'] )
&& $el_type === $preset['replacements']['custom']['originalWidget'];
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace Elementor\Core\Editor\Loader\Common;
use Elementor\Api;
use Elementor\Core\Debug\Loading_Inspection_Manager;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Group_Control_Typography;
use Elementor\Icons_Manager;
use Elementor\Modules\Apps\Module as AppsModule;
use Elementor\Core\Common\Modules\EventsManager\Module as EditorEventsModule;
use Elementor\Modules\Home\Module as Home_Module;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\Shapes;
use Elementor\Tools;
use Elementor\User;
use Elementor\Utils;
use Elementor\Core\Utils\Hints;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Editor_Common_Scripts_Settings {
public static function get() {
$settings = SettingsManager::get_settings_managers_config();
// Moved to document since 2.9.0.
unset( $settings['page'] );
$document = Plugin::$instance->documents->get_doc_or_auto_save( Plugin::$instance->editor->get_post_id() );
$kits_manager = Plugin::$instance->kits_manager;
$page_title_selector = $kits_manager->get_current_settings( 'page_title_selector' );
$top_bar_connect_app = Plugin::$instance->common->get_component( 'connect' )->get_app( 'activate' ) ?? Plugin::$instance->common->get_component( 'connect' )->get_app( 'library' );
$page_title_selector .= ', .elementor-page-title .elementor-heading-title';
$client_env = [
'initial_document' => $document->get_config(),
'version' => ELEMENTOR_VERSION,
'home_url' => home_url(),
'admin_settings_url' => admin_url( 'admin.php?page=' . Home_Module::get_elementor_settings_page_id() ),
'admin_tools_url' => admin_url( 'admin.php?page=' . Tools::PAGE_ID ),
'admin_apps_url' => admin_url( 'admin.php?page=' . AppsModule::PAGE_ID ),
'autosave_interval' => AUTOSAVE_INTERVAL,
'tabs' => Plugin::$instance->controls_manager->get_tabs(),
'controls' => Plugin::$instance->controls_manager->get_controls_data(),
'elements' => Plugin::$instance->elements_manager->get_element_types_config(),
'globals' => [
'defaults_enabled' => [
'colors' => $kits_manager->is_custom_colors_enabled(),
'typography' => $kits_manager->is_custom_typography_enabled(),
],
],
'icons' => [
'libraries' => Icons_Manager::get_icon_manager_tabs_config(),
'goProURL' => 'https://go.elementor.com/go-pro-icon-library/',
],
'fa4_to_fa5_mapping_url' => ELEMENTOR_ASSETS_URL . 'lib/font-awesome/migration/mapping.js',
'settings' => $settings,
'wp_editor' => static::get_wp_editor_config(),
'settings_page_link' => Settings::get_url(),
'tools_page_link' => Tools::get_url(),
'tools_page_nonce' => wp_create_nonce( 'tools-page-from-editor' ),
'elementor_site' => 'https://go.elementor.com/about-elementor/',
'docs_elementor_site' => 'https://go.elementor.com/docs/',
'help_the_content_url' => 'https://go.elementor.com/the-content-missing/',
'help_flexbox_bc_url' => 'https://go.elementor.com/flexbox-layout-bc/',
'elementPromotionURL' => 'https://go.elementor.com/go-pro-%s',
'dynamicPromotionURL' => 'https://go.elementor.com/go-pro-dynamic-tag',
'additional_shapes' => Shapes::get_additional_shapes_for_config(),
'user' => [
'restrictions' => Plugin::$instance->role_manager->get_user_restrictions_array(),
'is_administrator' => current_user_can( 'manage_options' ),
'introduction' => User::get_introduction_meta(),
'dismissed_editor_notices' => User::get_dismissed_editor_notices(),
'locale' => get_user_locale(),
'top_bar' => [
'connect_url' => $top_bar_connect_app->get_admin_url( 'authorize', [
'utm_source' => 'editor-app',
'utm_campaign' => 'connect-account',
'utm_medium' => 'wp-dash',
'utm_term' => '1.0.0',
'utm_content' => 'cta-link',
'source' => 'generic',
'mode' => 'popup',
] ),
'my_elementor_url' => 'https://go.elementor.com/wp-dash-top-bar-account/',
],
],
'preview' => [
'help_preview_error_url' => 'https://go.elementor.com/preview-not-loaded/',
'help_preview_http_error_url' => 'https://go.elementor.com/preview-not-loaded/#permissions',
'help_preview_http_error_500_url' => 'https://go.elementor.com/500-error/',
'debug_data' => Loading_Inspection_Manager::instance()->run_inspections(),
],
'locale' => get_locale(),
'rich_editing_enabled' => filter_var( get_user_meta( get_current_user_id(), 'rich_editing', true ), FILTER_VALIDATE_BOOLEAN ),
'page_title_selector' => $page_title_selector,
'tinymceHasCustomConfig' => class_exists( 'Tinymce_Advanced' ) || class_exists( 'Advanced_Editor_Tools' ),
'inlineEditing' => Plugin::$instance->widgets_manager->get_inline_editing_config(),
'dynamicTags' => Plugin::$instance->dynamic_tags->get_config(),
'ui' => [
'defaultGenericFonts' => $kits_manager->get_current_settings( 'default_generic_fonts' ),
],
// Empty array for BC to avoid errors.
'i18n' => [],
// 'responsive' contains the custom breakpoints config introduced in Elementor v3.2.0
'responsive' => [
'breakpoints' => Plugin::$instance->breakpoints->get_breakpoints_config(),
'icons_map' => Plugin::$instance->breakpoints->get_responsive_icons_classes_map(),
],
'promotion' => [
'elements' => Plugin::$instance->editor->promotion->get_elements_promotion(),
'integration' => [
'ally-accessibility' => Hints::get_ally_action_data(),
],
],
'editor_events' => EditorEventsModule::get_editor_events_config(),
'promotions' => [
'notes' => Filtered_Promotions_Manager::get_filtered_promotion_data(
[ 'upgrade_url' => 'https://go.elementor.com/go-pro-notes/' ],
'elementor/panel/notes/custom_promotion',
'upgrade_url'
),
],
'fontVariableRanges' => Group_Control_Typography::get_font_variable_ranges(),
];
if ( Plugin::$instance->experiments->is_feature_active( 'container' ) ) {
$client_env['elementsPresets'] = Plugin::$instance->editor->get_elements_presets();
}
$is_admin_user_without_pro = current_user_can( 'manage_options' ) && ! Utils::has_pro();
if ( $is_admin_user_without_pro ) {
$client_env['integrationWidgets'] = array_merge(
( isset( $client_env['integrationWidgets'] ) && is_array( $client_env['integrationWidgets'] ) ?
$client_env['integrationWidgets'] :
[] ), [
[
'categories' => '[ "general" ]',
'icon' => 'eicon-accessibility',
'name' => 'ally-accessibility',
'title' => esc_html__( 'Ally Accessibility', 'elementor' ),
'keywords' => [
'Accessibility',
'Usability',
'Inclusive',
'Statement',
'WCAG',
'Ally',
'Complaince',
],
],
],
);
}
static::bc_move_document_filters();
/**
* Localize editor settings.
*
* Filters the editor localized settings.
*
* @since 1.0.0
*
* @param array $client_env Editor configuration.
* @param int $post_id The ID of the current post being edited.
*/
$client_env = apply_filters( 'elementor/editor/localize_settings', $client_env );
if ( $is_admin_user_without_pro ) {
$client_env = self::ensure_pro_widgets( $client_env );
}
if ( ! empty( $client_env['promotionWidgets'] ) && is_array( $client_env['promotionWidgets'] ) ) {
$client_env['promotionWidgets'] = self::ensure_numeric_keys( $client_env['promotionWidgets'] );
}
return $client_env;
}
private static function ensure_pro_widgets( array $client_env ) {
$pro_widgets = Api::get_promotion_widgets();
if ( ! isset( $client_env['promotionWidgets'] ) ) {
$client_env['promotionWidgets'] = $pro_widgets;
} else {
$client_env['promotionWidgets'] = array_merge( $pro_widgets, $client_env['promotionWidgets'] );
}
return $client_env;
}
private static function ensure_numeric_keys( array $base_array ) {
return array_values( $base_array );
}
private static function bc_move_document_filters() {
global $wp_filter;
$old_tag = 'elementor/editor/localize_settings';
$new_tag = 'elementor/document/config';
if ( ! has_filter( $old_tag ) ) {
return;
}
foreach ( $wp_filter[ $old_tag ] as $priority => $filters ) {
foreach ( $filters as $filter_id => $filter_args ) {
if ( 2 === $filter_args['accepted_args'] ) {
remove_filter( $old_tag, $filter_id, $priority );
add_filter( $new_tag, $filter_args['function'], $priority, 2 );
}
}
}
}
/**
* Get WordPress editor config.
*
* Config the default WordPress editor with custom settings for Elementor use.
*
* @since 1.9.0
* @access private
*/
private static function get_wp_editor_config() {
// Remove all TinyMCE plugins.
remove_all_filters( 'mce_buttons', 10 );
remove_all_filters( 'mce_external_plugins', 10 );
if ( ! class_exists( '\_WP_Editors', false ) ) {
require ABSPATH . WPINC . '/class-wp-editor.php';
}
// WordPress 4.8 and higher
if ( method_exists( '\_WP_Editors', 'print_tinymce_scripts' ) ) {
\_WP_Editors::print_default_editor_scripts();
\_WP_Editors::print_tinymce_scripts();
}
ob_start();
wp_editor(
'%%EDITORCONTENT%%',
'elementorwpeditor',
[
'editor_class' => 'elementor-wp-editor',
'editor_height' => 250,
'drag_drop_upload' => true,
]
);
$config = ob_get_clean();
// Don't call \_WP_Editors methods again
remove_action( 'admin_print_footer_scripts', [ '_WP_Editors', 'editor_js' ], 50 );
remove_action( 'admin_print_footer_scripts', [ '_WP_Editors', 'print_default_editor_scripts' ], 45 );
\_WP_Editors::editor_js();
return $config;
}
}

View File

@@ -0,0 +1,288 @@
<?php
namespace Elementor\Core\Editor\Loader;
use Elementor\Core\Utils\Assets_Config_Provider;
use Elementor\Core\Utils\Collection;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Editor_Base_Loader implements Editor_Loader_Interface {
/**
* @var Collection
*/
protected $config;
/**
* @var Assets_Config_Provider
*/
protected $assets_config_provider;
/**
* @param Collection $config
* @param Assets_Config_Provider $assets_config_provider
*/
public function __construct( Collection $config, Assets_Config_Provider $assets_config_provider ) {
$this->config = $config;
$this->assets_config_provider = $assets_config_provider;
}
/**
* @return void
*/
public function register_scripts() {
$assets_url = $this->config->get( 'assets_url' );
$min_suffix = $this->config->get( 'min_suffix' );
wp_register_script(
'elementor-editor-modules',
"{$assets_url}js/editor-modules{$min_suffix}.js",
[ 'elementor-common-modules' ],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'elementor-editor-document',
"{$assets_url}js/editor-document{$min_suffix}.js",
[ 'elementor-common-modules' ],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'perfect-scrollbar',
"{$assets_url}lib/perfect-scrollbar/js/perfect-scrollbar{$min_suffix}.js",
[],
'1.4.0',
true
);
wp_register_script(
'jquery-easing',
"{$assets_url}lib/jquery-easing/jquery-easing{$min_suffix}.js",
[ 'jquery' ],
'1.3.2',
true
);
wp_register_script(
'nprogress',
"{$assets_url}lib/nprogress/nprogress{$min_suffix}.js",
[],
'0.2.0',
true
);
wp_register_script(
'tipsy',
"{$assets_url}lib/tipsy/tipsy{$min_suffix}.js",
[ 'jquery' ],
'1.0.0',
true
);
wp_register_script(
'jquery-elementor-select2',
"{$assets_url}lib/e-select2/js/e-select2.full{$min_suffix}.js",
[ 'jquery' ],
'4.0.6-rc.1',
true
);
wp_register_script(
'flatpickr',
"{$assets_url}lib/flatpickr/flatpickr{$min_suffix}.js",
[ 'jquery' ],
'4.6.13',
true
);
wp_register_script(
'ace',
'https://cdn.jsdelivr.net/npm/ace-builds@1.43.2/src-min-noconflict/ace.min.js',
[],
'1.43.2',
true
);
wp_register_script(
'ace-language-tools',
'https://cdn.jsdelivr.net/npm/ace-builds@1.43.2/src-min-noconflict/ext-language_tools.js',
[ 'ace' ],
'1.43.2',
true
);
wp_register_script(
'jquery-hover-intent',
"{$assets_url}lib/jquery-hover-intent/jquery-hover-intent{$min_suffix}.js",
[],
'1.0.0',
true
);
wp_register_script(
'nouislider',
"{$assets_url}lib/nouislider/nouislider{$min_suffix}.js",
[],
'13.0.0',
true
);
wp_register_script(
'pickr',
"{$assets_url}lib/pickr/pickr.min.js",
[],
'1.8.2',
true
);
wp_register_script(
'elementor-editor',
"{$assets_url}js/editor{$min_suffix}.js",
[
'elementor-common',
'elementor-editor-modules',
'elementor-editor-document',
'wp-auth-check',
'jquery-ui-sortable',
'jquery-ui-resizable',
'perfect-scrollbar',
'nprogress',
'tipsy',
'imagesloaded',
'heartbeat',
'jquery-elementor-select2',
'flatpickr',
'ace',
'ace-language-tools',
'jquery-hover-intent',
'nouislider',
'pickr',
'react',
'react-dom',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'elementor-editor', 'elementor' );
wp_register_script(
'elementor-responsive-bar',
"{$assets_url}js/responsive-bar{$min_suffix}.js",
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'elementor-responsive-bar', 'elementor' );
}
/**
* @return void
*/
public function enqueue_scripts() {
wp_enqueue_script( 'elementor-responsive-bar' );
}
/**
* @return void
*/
public function register_styles() {
$assets_url = $this->config->get( 'assets_url' );
$min_suffix = $this->config->get( 'min_suffix' );
$direction_suffix = $this->config->get( 'direction_suffix' );
wp_register_style(
'font-awesome',
"{$assets_url}lib/font-awesome/css/font-awesome{$min_suffix}.css",
[],
'4.7.0'
);
wp_register_style(
'elementor-select2',
"{$assets_url}lib/e-select2/css/e-select2{$min_suffix}.css",
[],
'4.0.6-rc.1'
);
wp_register_style(
'google-font-roboto',
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700',
[],
ELEMENTOR_VERSION
);
wp_register_style(
'flatpickr',
"{$assets_url}lib/flatpickr/flatpickr{$min_suffix}.css",
[],
'4.6.13'
);
wp_register_style(
'pickr',
"{$assets_url}lib/pickr/themes/monolith.min.css",
[],
'1.8.2'
);
wp_register_style(
'elementor-editor',
"{$assets_url}css/editor{$direction_suffix}{$min_suffix}.css",
[
'elementor-common',
'elementor-select2',
'elementor-icons',
'wp-auth-check',
'google-font-roboto',
'flatpickr',
'pickr',
],
ELEMENTOR_VERSION
);
wp_register_style(
'elementor-responsive-bar',
"{$assets_url}css/responsive-bar{$min_suffix}.css",
[],
ELEMENTOR_VERSION
);
}
/**
* @return void
*/
public function enqueue_styles() {
wp_enqueue_style( 'elementor-editor' );
wp_enqueue_style( 'elementor-responsive-bar' );
}
/**
* @return void
*/
public function register_additional_templates() {
$templates = [
'global',
'panel',
'panel-elements',
'repeater',
'templates',
'navigator',
'hotkeys',
'responsive-bar',
];
$templates = apply_filters( 'elementor/editor/templates', $templates );
foreach ( $templates as $template ) {
Plugin::$instance->common->add_template( ELEMENTOR_PATH . "includes/editor-templates/{$template}.php" );
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\Core\Editor\Loader;
use Elementor\Core\Editor\Editor;
use Elementor\Core\Editor\Loader\V1\Editor_V1_Loader;
use Elementor\Core\Editor\Loader\V2\Editor_V2_Loader;
use Elementor\Core\Utils\Assets_Config_Provider;
use Elementor\Core\Utils\Collection;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Editor_Loader_Factory {
/**
* @return Editor_Loader_Interface
*/
public static function create() {
$config = new Collection( [
'assets_url' => ELEMENTOR_ASSETS_URL,
'min_suffix' => ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min',
'direction_suffix' => is_rtl() ? '-rtl' : '',
] );
$assets_config_provider = ( new Assets_Config_Provider() )
->set_path_resolver( function ( $name ) {
return ELEMENTOR_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php";
} );
if ( static::should_use_v2_loader() ) {
return new Editor_V2_Loader( $config, $assets_config_provider );
}
return new Editor_V1_Loader( $config, $assets_config_provider );
}
/**
* If there are v2 packages enqueued, we should use the V2 loader.
*
* @return bool
*/
private static function should_use_v2_loader() {
return ! empty( Editor_V2_Loader::get_packages_to_enqueue() );
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Elementor\Core\Editor\Loader;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
interface Editor_Loader_Interface {
/**
* Init function purpose is to prepare some stuff that should be available for other methods
* and register some hooks
*
* @return void
*/
public function init();
/**
* Register all the scripts for the editor.
*
* @return void
*/
public function register_scripts();
/**
* Enqueue all the scripts for the editor.
*
* @return void
*/
public function enqueue_scripts();
/**
* Register all the styles for the editor.
*
* @return void
*/
public function register_styles();
/**
* Enqueue all the styles for the editor.
*
* @return void
*/
public function enqueue_styles();
/**
* Print the actual initial html for the editor, later on, the scripts takeover and renders the JS apps.
*
* @return void
*/
public function print_root_template();
/**
* Register additional templates that are required for the marionette part of the application
*
* @return void
*/
public function register_additional_templates();
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Elementor\Core\Editor\Loader\V1;
use Elementor\Core\Editor\Loader\Common\Editor_Common_Scripts_Settings;
use Elementor\Core\Editor\Loader\Editor_Base_Loader;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Editor_V1_Loader extends Editor_Base_Loader {
/**
* @return void
*/
public function init() {
// Loading UI and Icons v2 scrips for the use of new features that should live in V1.
$packages_to_register = [ 'ui', 'icons', 'query' ];
foreach ( $packages_to_register as $package ) {
$this->assets_config_provider->load( $package );
}
}
/**
* @return void
*/
public function register_scripts() {
parent::register_scripts();
$assets_url = $this->config->get( 'assets_url' );
$min_suffix = $this->config->get( 'min_suffix' );
foreach ( $this->assets_config_provider->all() as $package => $config ) {
wp_register_script(
$config['handle'],
"{$assets_url}js/packages/{$package}/{$package}{$min_suffix}.js",
$config['deps'],
ELEMENTOR_VERSION,
true
);
}
wp_register_script(
'elementor-editor-loader-v1',
"{$assets_url}js/editor-loader-v1{$min_suffix}.js",
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
}
/**
* @return void
*/
public function enqueue_scripts() {
parent::enqueue_scripts();
// Must be last.
wp_enqueue_script( 'elementor-editor-loader-v1' );
Utils::print_js_config(
'elementor-editor',
'ElementorConfig',
Editor_Common_Scripts_Settings::get()
);
}
/**
* @return void
*/
public function print_root_template() {
// Exposing the path for the view part to render the body of the editor template.
$body_file_path = __DIR__ . '/templates/editor-body-v1-view.php';
include ELEMENTOR_PATH . 'includes/editor-templates/editor-wrapper.php';
}
/**
* @return void
*/
public function register_additional_templates() {
parent::register_additional_templates();
Plugin::$instance->common->add_template( ELEMENTOR_PATH . 'includes/editor-templates/responsive-bar.php' );
}
}

View File

@@ -0,0 +1 @@
window.elementor.start();

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