first commit

This commit is contained in:
DESKTOP-GBA0BK8\Admin
2023-04-08 12:19:53 -04:00
commit 7c8c8b1c76
4586 changed files with 2050693 additions and 0 deletions
@@ -0,0 +1,570 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Core\Base\Module;
use Elementor\Plugin;
use Elementor\Tracker;
use Elementor\User;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin_Notices extends Module {
private $notices = [
'api_notice',
'api_upgrade_plugin',
'tracker',
'rate_us_feedback',
'woocommerce_promote',
'cf7_promote',
'mc4wp_promote',
'popup_maker_promote',
'role_manager_promote',
];
private $elementor_pages_count = null;
private $install_time = null;
private $current_screen_id = null;
private function get_install_time() {
if ( null === $this->install_time ) {
$this->install_time = Plugin::$instance->get_install_time();
}
return $this->install_time;
}
private function get_elementor_pages_count() {
if ( null === $this->elementor_pages_count ) {
$elementor_pages = new \WP_Query( [
'post_type' => 'any',
'post_status' => 'publish',
'fields' => 'ids',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'meta_key' => '_elementor_edit_mode',
'meta_value' => 'builder',
] );
$this->elementor_pages_count = $elementor_pages->post_count;
}
return $this->elementor_pages_count;
}
private function notice_api_upgrade_plugin() {
$upgrade_notice = Api::get_upgrade_notice();
if ( empty( $upgrade_notice ) ) {
return false;
}
if ( ! current_user_can( 'update_plugins' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'toplevel_page_elementor', 'edit-elementor_library', 'elementor_page_elementor-system-info', 'dashboard' ], true ) ) {
return false;
}
// Check if have any upgrades.
$update_plugins = get_site_transient( 'update_plugins' );
$has_remote_update_package = ! ( empty( $update_plugins ) || empty( $update_plugins->response[ ELEMENTOR_PLUGIN_BASE ] ) || empty( $update_plugins->response[ ELEMENTOR_PLUGIN_BASE ]->package ) );
if ( ! $has_remote_update_package && empty( $upgrade_notice['update_link'] ) ) {
return false;
}
if ( $has_remote_update_package ) {
$product = $update_plugins->response[ ELEMENTOR_PLUGIN_BASE ];
$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $product->slug . '&section=changelog&TB_iframe=true&width=600&height=800' );
$upgrade_url = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . ELEMENTOR_PLUGIN_BASE ), 'upgrade-plugin_' . ELEMENTOR_PLUGIN_BASE );
$new_version = $product->new_version;
} else {
$upgrade_url = $upgrade_notice['update_link'];
$details_url = $upgrade_url;
$new_version = $upgrade_notice['version'];
}
// Check if have upgrade notices to show.
if ( version_compare( ELEMENTOR_VERSION, $upgrade_notice['version'], '>=' ) ) {
return false;
}
$notice_id = 'upgrade_notice_' . $upgrade_notice['version'];
if ( User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<strong><?php echo __( 'Update Notification', 'elementor' ); ?></strong>
<p>
<?php
printf(
/* translators: 1: Details URL, 2: Accessibility text, 3: Version number, 4: Update URL, 5: Accessibility text */
__( 'There is a new version of Elementor Page Builder available. <a href="%1$s" class="thickbox open-plugin-details-modal" aria-label="%2$s">View version %3$s details</a> or <a href="%4$s" class="update-link" aria-label="%5$s">update now</a>.', 'elementor' ),
esc_url( $details_url ),
esc_attr( sprintf(
/* translators: %s: Elementor version */
__( 'View Elementor version %s details', 'elementor' ),
$new_version
) ),
$new_version,
esc_url( $upgrade_url ),
esc_attr( __( 'Update Elementor Now', 'elementor' ) )
);
?>
</p>
</div>
<div class="elementor-message-action">
<a class="button elementor-button" href="<?php echo $upgrade_url; ?>">
<i class="dashicons dashicons-update" aria-hidden="true"></i>
<?php echo __( 'Update Now', 'elementor' ); ?>
</a>
</div>
</div>
</div>
<?php
return true;
}
private function notice_api_notice() {
$admin_notice = Api::get_admin_notice();
if ( empty( $admin_notice ) ) {
return false;
}
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'toplevel_page_elementor', 'edit-elementor_library', 'elementor_page_elementor-system-info', 'dashboard' ], true ) ) {
return false;
}
$notice_id = 'admin_notice_api_' . $admin_notice['notice_id'];
if ( User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
?>
<div class="notice is-dismissible updated elementor-message-dismissed elementor-message-announcement" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<p><?php echo $admin_notice['notice_text']; ?></p>
</div>
<?php
return true;
}
private function notice_tracker() {
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
// Show tracker notice after 24 hours from installed time.
if ( strtotime( '+24 hours', $this->get_install_time() ) > time() ) {
return false;
}
if ( '1' === get_option( 'elementor_tracker_notice' ) ) {
return false;
}
if ( Tracker::is_allow_track() ) {
return false;
}
if ( 2 > $this->get_elementor_pages_count() ) {
return false;
}
// TODO: Skip for development env.
$optin_url = wp_nonce_url( add_query_arg( 'elementor_tracker', 'opt_into' ), 'opt_into' );
$optout_url = wp_nonce_url( add_query_arg( 'elementor_tracker', 'opt_out' ), 'opt_out' );
$tracker_description_text = __( 'Love using Elementor? Become a super contributor by opting in to share non-sensitive plugin data and to receive periodic email updates from us.', 'elementor' );
/**
* Tracker admin description text.
*
* Filters the admin notice text for non-sensitive data collection.
*
* @since 1.0.0
*
* @param string $tracker_description_text Description text displayed in admin notice.
*/
$tracker_description_text = apply_filters( 'elementor/tracker/admin_description_text', $tracker_description_text );
?>
<div class="notice updated elementor-message">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php echo esc_html( $tracker_description_text ); ?> <a href="https://go.elementor.com/usage-data-tracking/" target="_blank"><?php echo __( 'Learn more.', 'elementor' ); ?></a></p>
<p class="elementor-message-actions">
<a href="<?php echo $optin_url; ?>" class="button button-primary"><?php echo __( 'Sure! I\'d love to help', 'elementor' ); ?></a>&nbsp;<a href="<?php echo $optout_url; ?>" class="button-secondary"><?php echo __( 'No thanks', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_rate_us_feedback() {
$notice_id = 'rate_us_feedback';
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
if ( 'dashboard' !== $this->current_screen_id || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
if ( 10 >= $this->get_elementor_pages_count() ) {
return false;
}
$dismiss_url = add_query_arg( [
'action' => 'elementor_set_admin_notice_viewed',
'notice_id' => esc_attr( $notice_id ),
], admin_url( 'admin-post.php' ) );
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><strong><?php echo __( 'Congrats!', 'elementor' ); ?></strong> <?php _e( 'You created over 10 pages with Elementor. Great job! If you can spare a minute, please help us by leaving a five star review on WordPress.org.', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/admin-review/" target="_blank" class="button button-primary"><?php _e( 'Happy To Help', 'elementor' ); ?></a>
<a href="<?php echo esc_url_raw( $dismiss_url ); ?>" class="button elementor-button-notice-dismiss"><?php _e( 'Hide Notification', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_woocommerce_promote() {
$notice_id = 'woocommerce_promote';
if ( Utils::has_pro() || ! function_exists( 'WC' ) ) {
return false;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'edit-product', 'woocommerce_page_wc-settings' ], true ) || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
if ( strtotime( '2019-08-01' ) > $this->get_install_time() ) {
return false;
}
if ( strtotime( '+24 hours', $this->get_install_time() ) > time() ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php _e( 'Using WooCommerce? With Elementor Pros WooCommerce Builder, youll be able to design your store without coding!', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/plugin-promotion-woocommerce/" target="_blank" class="button button-secondary"><?php _e( 'Learn More', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_cf7_promote() {
$notice_id = 'cf7_promote';
if ( Utils::has_pro() || ! defined( 'WPCF7_VERSION' ) ) {
return false;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'toplevel_page_wpcf7', 'contact_page_wpcf7-integration' ], true ) || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
if ( strtotime( '2019-08-01' ) > $this->get_install_time() ) {
return false;
}
if ( strtotime( '+24 hours', $this->get_install_time() ) > time() ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php _e( 'Using Elementor & Contact Form 7? Try out Elementor Pro and design your forms visually with one powerful tool.', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/plugin-promotion-contactform7/" target="_blank" class="button button-secondary"><?php _e( 'Learn More', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_mc4wp_promote() {
$notice_id = 'mc4wp_promote';
if ( Utils::has_pro() || ! defined( 'MC4WP_VERSION' ) ) {
return false;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'toplevel_page_mailchimp-for-wp', 'mc4wp_page_mailchimp-for-wp-forms', 'mc4wp_page_mailchimp-for-wp-integrations', 'mc4wp_page_mailchimp-for-wp-other', 'mc4wp_page_mailchimp-for-wp-extensions' ], true ) || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
if ( strtotime( '2019-08-01' ) > $this->get_install_time() ) {
return false;
}
if ( strtotime( '+24 hours', $this->get_install_time() ) > time() ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php _e( 'Want to design better MailChimp forms? Use Elementor Pro and enjoy unlimited integrations, visual design, templates and more.', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/plugin-promotion-mc4wp/" target="_blank" class="button button-secondary"><?php _e( 'Learn More', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_popup_maker_promote() {
$notice_id = 'popup_maker_promote';
if ( Utils::has_pro() || ! class_exists( 'Popup_Maker' ) ) {
return false;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
if ( ! in_array( $this->current_screen_id, [ 'edit-popup', 'popup_page_pum-settings' ], true ) || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
if ( strtotime( '2019-08-01' ) > $this->get_install_time() ) {
return false;
}
if ( strtotime( '+24 hours', $this->get_install_time() ) > time() ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php _e( 'Using popups on your site? Build outstanding popups using Elementor Pro and get more leads, sales and subscribers.', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/plugin-promotion-popupmaker/" target="_blank" class="button button-secondary"><?php _e( 'Learn More', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
private function notice_role_manager_promote() {
$notice_id = 'role_manager_promote';
if ( Utils::has_pro() ) {
return false;
}
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
if ( 'elementor_page_elementor-role-manager' !== $this->current_screen_id || User::is_user_notice_viewed( $notice_id ) ) {
return false;
}
$users = new \WP_User_Query( [
'fields' => 'ID',
'number' => 10,
] );
if ( 5 > $users->get_total() ) {
return false;
}
?>
<div class="notice updated is-dismissible elementor-message elementor-message-dismissed" data-notice_id="<?php echo esc_attr( $notice_id ); ?>">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<p><?php _e( 'Managing a multi-user site? With Elementor Pro, you can control user access and make sure no one messes up your design.', 'elementor' ); ?></p>
<p class="elementor-message-actions">
<a href="https://go.elementor.com/promotion-role-manager/" target="_blank" class="button button-secondary"><?php _e( 'Learn More', 'elementor' ); ?></a>
</p>
</div>
</div>
</div>
<?php
return true;
}
/*
* @TODO: Rewrite this method markup and use it for every admin notice
*/
public function print_admin_notice( array $options ) {
$default_options = [
'title' => '',
'description' => '',
'button' => [
'text' => '',
'url' => '',
'class' => 'elementor-button',
],
];
$options = array_replace_recursive( $default_options, $options );
?>
<div class="notice elementor-message">
<div class="elementor-message-inner">
<div class="elementor-message-icon">
<div class="e-logo-wrapper">
<i class="eicon-elementor" aria-hidden="true"></i>
</div>
</div>
<div class="elementor-message-content">
<?php if ( $options['title'] ) { ?>
<strong><?php echo $options['title']; ?></strong>
<?php } ?>
<?php if ( $options['description'] ) { ?>
<p><?php echo $options['description']; ?></p>
<?php } ?>
</div>
<?php if ( $options['button']['text'] ) { ?>
<div class="elementor-message-action">
<a class="<?php echo $options['button']['class']; ?>" href="<?php echo esc_url( $options['button']['url'] ); ?>"><?php echo $options['button']['text']; ?></a>
</div>
<?php } ?>
</div>
</div>
<?php
}
public function admin_notices() {
$this->install_time = Plugin::$instance->get_install_time();
$this->current_screen_id = get_current_screen()->id;
foreach ( $this->notices as $notice ) {
$method_callback = "notice_{$notice}";
if ( $this->$method_callback() ) {
return;
}
}
}
/**
* @since 2.9.0
* @access public
*/
public function __construct() {
add_action( 'admin_notices', [ $this, 'admin_notices' ], 20 );
}
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 2.9.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'admin-notices';
}
}
@@ -0,0 +1,734 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Beta_Testers;
use Elementor\Core\Base\App;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\User;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin extends App {
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 2.3.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'admin';
}
/**
* @since 2.2.0
* @access public
*/
public function maybe_redirect_to_getting_started() {
if ( ! get_transient( 'elementor_activation_redirect' ) ) {
return;
}
if ( wp_doing_ajax() ) {
return;
}
delete_transient( 'elementor_activation_redirect' );
if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) {
return;
}
global $wpdb;
$has_elementor_page = ! ! $wpdb->get_var( "SELECT `post_id` FROM `{$wpdb->postmeta}` WHERE `meta_key` = '_elementor_edit_mode' LIMIT 1;" );
if ( $has_elementor_page ) {
return;
}
wp_safe_redirect( admin_url( 'admin.php?page=elementor-getting-started' ) );
exit;
}
/**
* Enqueue admin scripts.
*
* Registers all the admin scripts and enqueues them.
*
* Fired by `admin_enqueue_scripts` action.
*
* @since 1.0.0
* @access public
*/
public function enqueue_scripts() {
wp_register_script(
'elementor-admin',
$this->get_js_assets_url( 'admin' ),
[
'elementor-common',
],
ELEMENTOR_VERSION,
true
);
wp_enqueue_script( 'elementor-admin' );
$this->print_config();
}
/**
* Enqueue admin styles.
*
* Registers all the admin styles and enqueues them.
*
* Fired by `admin_enqueue_scripts` action.
*
* @since 1.0.0
* @access public
*/
public function enqueue_styles() {
$direction_suffix = is_rtl() ? '-rtl' : '';
wp_register_style(
'elementor-admin',
$this->get_css_assets_url( 'admin' . $direction_suffix ),
[
'elementor-common',
],
ELEMENTOR_VERSION
);
wp_enqueue_style( 'elementor-admin' );
// It's for upgrade notice.
// TODO: enqueue this just if needed.
add_thickbox();
}
/**
* Print switch mode button.
*
* Adds a switch button in post edit screen (which has cpt support). To allow
* the user to switch from the native WordPress editor to Elementor builder.
*
* Fired by `edit_form_after_title` action.
*
* @since 1.0.0
* @access public
*
* @param \WP_Post $post The current post object.
*/
public function print_switch_mode_button( $post ) {
// Exit if Gutenberg are active.
if ( did_action( 'enqueue_block_editor_assets' ) ) {
return;
}
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document || ! $document->is_editable_by_current_user() ) {
return;
}
wp_nonce_field( basename( __FILE__ ), '_elementor_edit_mode_nonce' );
?>
<div id="elementor-switch-mode">
<input id="elementor-switch-mode-input" type="hidden" name="_elementor_post_mode" value="<?php echo $document->is_built_with_elementor(); ?>" />
<button id="elementor-switch-mode-button" type="button" class="button button-primary button-hero">
<span class="elementor-switch-mode-on">
<i class="eicon-arrow-<?php echo ( is_rtl() ) ? 'right' : 'left'; ?>" aria-hidden="true"></i>
<?php echo __( 'Back to WordPress Editor', 'elementor' ); ?>
</span>
<span class="elementor-switch-mode-off">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo __( 'Edit with Elementor', 'elementor' ); ?>
</span>
</button>
</div>
<div id="elementor-editor">
<a id="elementor-go-to-edit-page-link" href="<?php echo $document->get_edit_url(); ?>">
<div id="elementor-editor-button" class="button button-primary button-hero">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo __( 'Edit with Elementor', 'elementor' ); ?>
</div>
<div class="elementor-loader-wrapper">
<div class="elementor-loader">
<div class="elementor-loader-boxes">
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
</div>
</div>
<div class="elementor-loading-title"><?php echo __( 'Loading', 'elementor' ); ?></div>
</div>
</a>
</div>
<?php
}
/**
* Save post.
*
* Flag the post mode when the post is saved.
*
* Fired by `save_post` action.
*
* @since 1.0.0
* @access public
*
* @param int $post_id Post ID.
*/
public function save_post( $post_id ) {
if ( ! isset( $_POST['_elementor_edit_mode_nonce'] ) || ! wp_verify_nonce( $_POST['_elementor_edit_mode_nonce'], basename( __FILE__ ) ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
Plugin::$instance->db->set_is_elementor_page( $post_id, ! empty( $_POST['_elementor_post_mode'] ) );
}
/**
* Add Elementor post state.
*
* Adds a new "Elementor" post state to the post table.
*
* Fired by `display_post_states` filter.
*
* @since 1.8.0
* @access public
*
* @param array $post_states An array of post display states.
* @param \WP_Post $post The current post object.
*
* @return array A filtered array of post display states.
*/
public function add_elementor_post_state( $post_states, $post ) {
if ( User::is_current_user_can_edit( $post->ID ) && Plugin::$instance->db->is_built_with_elementor( $post->ID ) ) {
$post_states['elementor'] = __( 'Elementor', 'elementor' );
}
return $post_states;
}
/**
* Body status classes.
*
* Adds CSS classes to the admin body tag.
*
* Fired by `admin_body_class` filter.
*
* @since 1.0.0
* @access public
*
* @param string $classes Space-separated list of CSS classes.
*
* @return string Space-separated list of CSS classes.
*/
public function body_status_classes( $classes ) {
global $pagenow;
if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) && Utils::is_post_support() ) {
$post = get_post();
$mode_class = Plugin::$instance->db->is_built_with_elementor( $post->ID ) ? 'elementor-editor-active' : 'elementor-editor-inactive';
$classes .= ' ' . $mode_class;
}
return $classes;
}
/**
* Plugin action links.
*
* Adds action links to the plugin list table
*
* Fired by `plugin_action_links` filter.
*
* @since 1.0.0
* @access public
*
* @param array $links An array of plugin action links.
*
* @return array An array of plugin action links.
*/
public function plugin_action_links( $links ) {
$settings_link = sprintf( '<a href="%1$s">%2$s</a>', admin_url( 'admin.php?page=' . Settings::PAGE_ID ), __( 'Settings', 'elementor' ) );
array_unshift( $links, $settings_link );
$links['go_pro'] = sprintf( '<a href="%1$s" target="_blank" class="elementor-plugins-gopro">%2$s</a>', Utils::get_pro_link( 'https://elementor.com/pro/?utm_source=wp-plugins&utm_campaign=gopro&utm_medium=wp-dash' ), __( 'Go Pro', 'elementor' ) );
return $links;
}
/**
* Plugin row meta.
*
* Adds row meta links to the plugin list table
*
* Fired by `plugin_row_meta` filter.
*
* @since 1.1.4
* @access public
*
* @param array $plugin_meta An array of the plugin's metadata, including
* the version, author, author URI, and plugin URI.
* @param string $plugin_file Path to the plugin file, relative to the plugins
* directory.
*
* @return array An array of plugin row meta links.
*/
public function plugin_row_meta( $plugin_meta, $plugin_file ) {
if ( ELEMENTOR_PLUGIN_BASE === $plugin_file ) {
$row_meta = [
'docs' => '<a href="https://go.elementor.com/docs-admin-plugins/" aria-label="' . esc_attr( __( 'View Elementor Documentation', 'elementor' ) ) . '" target="_blank">' . __( 'Docs & FAQs', 'elementor' ) . '</a>',
'ideo' => '<a href="https://go.elementor.com/yt-admin-plugins/" aria-label="' . esc_attr( __( 'View Elementor Video Tutorials', 'elementor' ) ) . '" target="_blank">' . __( 'Video Tutorials', 'elementor' ) . '</a>',
];
$plugin_meta = array_merge( $plugin_meta, $row_meta );
}
return $plugin_meta;
}
/**
* Admin footer text.
*
* Modifies the "Thank you" text displayed in the admin footer.
*
* Fired by `admin_footer_text` filter.
*
* @since 1.0.0
* @access public
*
* @param string $footer_text The content that will be printed.
*
* @return string The content that will be printed.
*/
public function admin_footer_text( $footer_text ) {
$current_screen = get_current_screen();
$is_elementor_screen = ( $current_screen && false !== strpos( $current_screen->id, 'elementor' ) );
if ( $is_elementor_screen ) {
$footer_text = sprintf(
/* translators: 1: Elementor, 2: Link to plugin review */
__( 'Enjoyed %1$s? Please leave us a %2$s rating. We really appreciate your support!', 'elementor' ),
'<strong>' . __( 'Elementor', 'elementor' ) . '</strong>',
'<a href="https://go.elementor.com/admin-review/" target="_blank">&#9733;&#9733;&#9733;&#9733;&#9733;</a>'
);
}
return $footer_text;
}
/**
* Register dashboard widgets.
*
* Adds a new Elementor widgets to WordPress dashboard.
*
* Fired by `wp_dashboard_setup` action.
*
* @since 1.9.0
* @access public
*/
public function register_dashboard_widgets() {
wp_add_dashboard_widget( 'e-dashboard-overview', __( 'Elementor Overview', 'elementor' ), [ $this, 'elementor_dashboard_overview_widget' ] );
// Move our widget to top.
global $wp_meta_boxes;
$dashboard = $wp_meta_boxes['dashboard']['normal']['core'];
$ours = [
'e-dashboard-overview' => $dashboard['e-dashboard-overview'],
];
$wp_meta_boxes['dashboard']['normal']['core'] = array_merge( $ours, $dashboard ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
/**
* Elementor dashboard widget.
*
* Displays the Elementor dashboard widget.
*
* Fired by `wp_add_dashboard_widget` function.
*
* @since 1.9.0
* @access public
*/
public function elementor_dashboard_overview_widget() {
$elementor_feed = Api::get_feed_data();
$recently_edited_query_args = [
'post_type' => 'any',
'post_status' => [ 'publish', 'draft' ],
'posts_per_page' => '3',
'meta_key' => '_elementor_edit_mode',
'meta_value' => 'builder',
'orderby' => 'modified',
];
$recently_edited_query = new \WP_Query( $recently_edited_query_args );
if ( User::is_current_user_can_edit_post_type( 'page' ) ) {
$create_new_label = __( 'Create New Page', 'elementor' );
$create_new_post_type = 'page';
} elseif ( User::is_current_user_can_edit_post_type( 'post' ) ) {
$create_new_label = __( 'Create New Post', 'elementor' );
$create_new_post_type = 'post';
}
?>
<div class="e-dashboard-widget">
<div class="e-overview__header">
<div class="e-overview__logo"><div class="e-logo-wrapper"><i class="eicon-elementor"></i></div></div>
<div class="e-overview__versions">
<span class="e-overview__version"><?php echo __( 'Elementor', 'elementor' ); ?> v<?php echo ELEMENTOR_VERSION; ?></span>
<?php
/**
* Elementor dashboard widget after the version.
*
* Fires after Elementor version display in the dashboard widget.
*
* @since 1.9.0
*/
do_action( 'elementor/admin/dashboard_overview_widget/after_version' );
?>
</div>
<?php if ( ! empty( $create_new_post_type ) ) : ?>
<div class="e-overview__create">
<a href="<?php echo esc_url( Utils::get_create_new_post_url( $create_new_post_type ) ); ?>" class="button"><span aria-hidden="true" class="dashicons dashicons-plus"></span> <?php echo esc_html( $create_new_label ); ?></a>
</div>
<?php endif; ?>
</div>
<?php if ( $recently_edited_query->have_posts() ) : ?>
<div class="e-overview__recently-edited">
<h3 class="e-overview__heading"><?php echo __( 'Recently Edited', 'elementor' ); ?></h3>
<ul class="e-overview__posts">
<?php
while ( $recently_edited_query->have_posts() ) :
$recently_edited_query->the_post();
$document = Plugin::$instance->documents->get( get_the_ID() );
$date = date_i18n( _x( 'M jS', 'Dashboard Overview Widget Recently Date', 'elementor' ), get_the_modified_time( 'U' ) );
?>
<li class="e-overview__post">
<a href="<?php echo esc_attr( $document->get_edit_url() ); ?>" class="e-overview__post-link"><?php echo esc_html( get_the_title() ); ?> <span class="dashicons dashicons-edit"></span></a> <span><?php echo $date; ?>, <?php the_time(); ?></span>
</li>
<?php endwhile; ?>
</ul>
</div>
<?php endif; ?>
<?php if ( ! empty( $elementor_feed ) ) : ?>
<div class="e-overview__feed">
<h3 class="e-overview__heading"><?php echo __( 'News & Updates', 'elementor' ); ?></h3>
<ul class="e-overview__posts">
<?php foreach ( $elementor_feed as $feed_item ) : ?>
<li class="e-overview__post">
<a href="<?php echo esc_url( $feed_item['url'] ); ?>" class="e-overview__post-link" target="_blank">
<?php if ( ! empty( $feed_item['badge'] ) ) : ?>
<span class="e-overview__badge"><?php echo esc_html( $feed_item['badge'] ); ?></span>
<?php endif; ?>
<?php echo esc_html( $feed_item['title'] ); ?>
</a>
<p class="e-overview__post-description"><?php echo esc_html( $feed_item['excerpt'] ); ?></p>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="e-overview__footer">
<ul>
<?php foreach ( $this->get_dashboard_overview_widget_footer_actions() as $action_id => $action ) : ?>
<li class="e-overview__<?php echo esc_attr( $action_id ); ?>"><a href="<?php echo esc_attr( $action['link'] ); ?>" target="_blank"><?php echo esc_html( $action['title'] ); ?> <span class="screen-reader-text"><?php echo __( '(opens in a new window)', 'elementor' ); ?></span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php
}
/**
* Get elementor dashboard overview widget footer actions.
*
* Retrieves the footer action links displayed in elementor dashboard widget.
*
* @since 1.9.0
* @access private
*/
private function get_dashboard_overview_widget_footer_actions() {
$base_actions = [
'blog' => [
'title' => __( 'Blog', 'elementor' ),
'link' => 'https://go.elementor.com/overview-widget-blog/',
],
'help' => [
'title' => __( 'Help', 'elementor' ),
'link' => 'https://go.elementor.com/overview-widget-docs/',
],
];
$additions_actions = [
'go-pro' => [
'title' => __( 'Go Pro', 'elementor' ),
'link' => Utils::get_pro_link( 'https://elementor.com/pro/?utm_source=wp-overview-widget&utm_campaign=gopro&utm_medium=wp-dash' ),
],
];
/**
* Dashboard widget footer actions.
*
* Filters the additions actions displayed in Elementor dashboard widget.
*
* Developers can add new action links to Elementor dashboard widget
* footer using this filter.
*
* @since 1.9.0
*
* @param array $additions_actions Elementor dashboard widget footer actions.
*/
$additions_actions = apply_filters( 'elementor/admin/dashboard_overview_widget/footer_actions', $additions_actions );
$actions = $base_actions + $additions_actions;
return $actions;
}
/**
* Admin action new post.
*
* When a new post action is fired the title is set to 'Elementor' and the post ID.
*
* Fired by `admin_action_elementor_new_post` action.
*
* @since 1.9.0
* @access public
*/
public function admin_action_new_post() {
check_admin_referer( 'elementor_action_new_post' );
if ( empty( $_GET['post_type'] ) ) {
$post_type = 'post';
} else {
$post_type = $_GET['post_type'];
}
if ( ! User::is_current_user_can_edit_post_type( $post_type ) ) {
return;
}
if ( empty( $_GET['template_type'] ) ) {
$type = 'post';
} else {
$type = sanitize_text_field( $_GET['template_type'] );
}
$post_data = isset( $_GET['post_data'] ) ? $_GET['post_data'] : [];
$meta = [];
/**
* Create new post meta data.
*
* Filters the meta data of any new post created.
*
* @since 2.0.0
*
* @param array $meta Post meta data.
*/
$meta = apply_filters( 'elementor/admin/create_new_post/meta', $meta );
$post_data['post_type'] = $post_type;
$document = Plugin::$instance->documents->create( $type, $post_data, $meta );
if ( is_wp_error( $document ) ) {
wp_die( $document );
}
wp_redirect( $document->get_edit_url() );
die;
}
/**
* @since 2.3.0
* @access public
*/
public function add_new_template_template() {
Plugin::$instance->common->add_template( ELEMENTOR_PATH . 'includes/admin-templates/new-template.php' );
}
/**
* @access public
*/
public function enqueue_new_template_scripts() {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script(
'elementor-new-template',
ELEMENTOR_ASSETS_URL . 'js/new-template' . $suffix . '.js',
[],
ELEMENTOR_VERSION,
true
);
}
/**
* @since 2.6.0
* @access public
*/
public function add_beta_tester_template() {
Plugin::$instance->common->add_template( ELEMENTOR_PATH . 'includes/admin-templates/beta-tester.php' );
}
/**
* @access public
*/
public function enqueue_beta_tester_scripts() {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script(
'elementor-beta-tester',
ELEMENTOR_ASSETS_URL . 'js/beta-tester' . $suffix . '.js',
[],
ELEMENTOR_VERSION,
true
);
}
/**
* @access public
*/
public function init_new_template() {
if ( 'edit-elementor_library' !== get_current_screen()->id ) {
return;
}
// Allow plugins to add their templates on admin_head.
add_action( 'admin_head', [ $this, 'add_new_template_template' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_new_template_scripts' ] );
}
public function version_update_warning( $current_version, $new_version ) {
$current_version_minor_part = explode( '.', $current_version )[1];
$new_version_minor_part = explode( '.', $new_version )[1];
if ( $current_version_minor_part === $new_version_minor_part ) {
return;
}
?>
<div class="e-major-update-warning">
<div class="e-major-update-warning__title"><?php echo __( 'Heads up, Please backup before upgrade!', 'elementor' ); ?></div>
<div class="e-major-update-warning__message"><?php echo __( 'The latest update includes some substantial changes across different areas of the plugin. We highly recommend you backup your site before upgrading, and make sure you first update in a staging environment', 'elementor' ); ?></div>
</div>
<?php
}
/**
* @access public
*/
public function init_beta_tester( $current_screen ) {
if ( ( 'toplevel_page_elementor' === $current_screen->base ) || 'elementor_page_elementor-tools' === $current_screen->id ) {
add_action( 'admin_head', [ $this, 'add_beta_tester_template' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_beta_tester_scripts' ] );
}
}
/**
* Admin constructor.
*
* Initializing Elementor in WordPress admin.
*
* @since 1.0.0
* @access public
*/
public function __construct() {
Plugin::$instance->init_common();
$this->add_component( 'feedback', new Feedback() );
$this->add_component( 'canary-deployment', new Canary_Deployment() );
$this->add_component( 'admin-notices', new Admin_Notices() );
add_action( 'admin_init', [ $this, 'maybe_redirect_to_getting_started' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] );
add_action( 'edit_form_after_title', [ $this, 'print_switch_mode_button' ] );
add_action( 'save_post', [ $this, 'save_post' ] );
add_filter( 'display_post_states', [ $this, 'add_elementor_post_state' ], 10, 2 );
add_filter( 'plugin_action_links_' . ELEMENTOR_PLUGIN_BASE, [ $this, 'plugin_action_links' ] );
add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 2 );
add_filter( 'admin_body_class', [ $this, 'body_status_classes' ] );
add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ] );
// Register Dashboard Widgets.
add_action( 'wp_dashboard_setup', [ $this, 'register_dashboard_widgets' ] );
// Admin Actions
add_action( 'admin_action_elementor_new_post', [ $this, 'admin_action_new_post' ] );
add_action( 'current_screen', [ $this, 'init_new_template' ] );
add_action( 'current_screen', [ $this, 'init_beta_tester' ] );
add_action( 'in_plugin_update_message-' . ELEMENTOR_PLUGIN_BASE, function( $plugin_data ) {
$this->version_update_warning( ELEMENTOR_VERSION, $plugin_data['new_version'] );
} );
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_init_settings() {
$beta_tester_email = get_user_meta( get_current_user_id(), User::BETA_TESTER_META_KEY, true );
$elementor_beta = get_option( 'elementor_beta', 'no' );
$all_introductions = User::get_introduction_meta();
$beta_tester_signup_dismissed = array_key_exists( Beta_Testers::BETA_TESTER_SIGNUP, $all_introductions );
$settings = [
'home_url' => home_url(),
'settings_url' => Settings::get_url(),
'i18n' => [
'rollback_confirm' => __( 'Are you sure you want to reinstall previous version?', 'elementor' ),
'rollback_to_previous_version' => __( 'Rollback to Previous Version', 'elementor' ),
'yes' => __( 'Continue', 'elementor' ),
'cancel' => __( 'Cancel', 'elementor' ),
'new_template' => __( 'New Template', 'elementor' ),
'back_to_wordpress_editor_message' => __( 'Please note that you are switching to WordPress default editor. Your current layout, design and content might break.', 'elementor' ),
'back_to_wordpress_editor_header' => __( 'Back to WordPress Editor', 'elementor' ),
'beta_tester_sign_up' => __( 'Sign Up', 'elementor' ),
'do_not_show_again' => __( 'Don\'t Show Again', 'elementor' ),
],
'user' => [
'introduction' => User::get_introduction_meta(),
],
'beta_tester' => [
'beta_tester_signup' => Beta_Testers::BETA_TESTER_SIGNUP,
'has_email' => $beta_tester_email,
'option_enabled' => 'no' !== $elementor_beta,
'signup_dismissed' => $beta_tester_signup_dismissed,
],
];
return apply_filters( 'elementor/admin/localize_settings', $settings );
}
}
@@ -0,0 +1,189 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Core\Base\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
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.Misspelled
// 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' ] );
}
}
@@ -0,0 +1,205 @@
<?php
namespace Elementor\Core\Admin;
use Elementor\Api;
use Elementor\Core\Base\Module;
use Elementor\Tracker;
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' ] );
add_filter( 'elementor/admin/localize_settings', [ $this, 'localize_feedback_dialog_settings' ] );
} );
// 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 = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_register_script(
'elementor-admin-feedback',
ELEMENTOR_ASSETS_URL . 'js/admin-feedback' . $suffix . '.js',
[
'elementor-common',
],
ELEMENTOR_VERSION,
true
);
wp_enqueue_script( 'elementor-admin-feedback' );
}
/**
* @since 2.3.0
* @access public
*/
public function localize_feedback_dialog_settings( $localized_settings ) {
$localized_settings['i18n']['submit_n_deactivate'] = __( 'Submit & Deactivate', 'elementor' );
$localized_settings['i18n']['skip_n_deactivate'] = __( 'Skip & Deactivate', 'elementor' );
return $localized_settings;
}
/**
* 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' => __( 'I no longer need the plugin', 'elementor' ),
'input_placeholder' => '',
],
'found_a_better_plugin' => [
'title' => __( 'I found a better plugin', 'elementor' ),
'input_placeholder' => __( 'Please share which plugin', 'elementor' ),
],
'couldnt_get_the_plugin_to_work' => [
'title' => __( 'I couldn\'t get the plugin to work', 'elementor' ),
'input_placeholder' => '',
],
'temporary_deactivation' => [
'title' => __( 'It\'s a temporary deactivation', 'elementor' ),
'input_placeholder' => '',
],
'elementor_pro' => [
'title' => __( 'I have Elementor Pro', 'elementor' ),
'input_placeholder' => '',
'alert' => __( 'Wait! Don\'t deactivate Elementor. You have to activate both Elementor and Elementor Pro in order for the plugin to work.', 'elementor' ),
],
'other' => [
'title' => __( 'Other', 'elementor' ),
'input_placeholder' => __( '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 __( '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 __( '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() {
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], '_elementor_deactivate_feedback_nonce' ) ) {
wp_send_json_error();
}
$reason_text = '';
$reason_key = '';
if ( ! empty( $_POST['reason_key'] ) ) {
$reason_key = $_POST['reason_key'];
}
if ( ! empty( $_POST[ "reason_{$reason_key}" ] ) ) {
$reason_text = $_POST[ "reason_{$reason_key}" ];
}
Api::send_feedback( $reason_key, $reason_text );
wp_send_json_success();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_init_settings() {
if ( ! $this->is_plugins_screen() ) {
return [];
}
return [ 'is_tracker_opted_in' => Tracker::is_allow_track() ];
}
/**
* @since 2.3.0
* @access private
*/
private function is_plugins_screen() {
return in_array( get_current_screen()->id, [ 'plugins', 'plugins-network' ] );
}
}
@@ -0,0 +1,238 @@
<?php
namespace Elementor\Core\App;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class App extends BaseApp {
const PAGE_ID = 'elementor-app';
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 3.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'app';
}
public function get_base_url() {
return admin_url( 'admin.php?page=' . self::PAGE_ID . '&ver=' . ELEMENTOR_VERSION );
}
public function register_admin_menu() {
add_submenu_page(
Source_Local::ADMIN_MENU_SLUG,
__( 'Theme Builder', 'elementor' ),
__( 'Theme Builder', 'elementor' ),
'manage_options',
self::PAGE_ID
);
}
public function fix_submenu( $menu ) {
global $submenu;
if ( is_multisite() && is_network_admin() ) {
return $menu;
}
// Non admin role / custom wp menu.
if ( empty( $submenu[ Source_Local::ADMIN_MENU_SLUG ] ) ) {
return $menu;
}
// Hack to add a link to sub menu.
foreach ( $submenu[ Source_Local::ADMIN_MENU_SLUG ] as &$item ) {
if ( self::PAGE_ID === $item[2] ) {
$item[2] = $this->get_settings( 'menu_url' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$item[4] = 'elementor-app-link'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
}
return $menu;
}
public function is_current() {
return ( ! empty( $_GET['page'] ) && self::PAGE_ID === $_GET['page'] );
}
public function admin_init() {
do_action( 'elementor/app/init', $this );
$this->enqueue_assets();
// Setup default heartbeat options
// TODO: Enable heartbeat.
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 15;
return $settings;
} );
$this->render();
die;
}
protected function get_init_settings() {
return [
'menu_url' => $this->get_base_url() . '#site-editor/promotion',
'assets_url' => ELEMENTOR_ASSETS_URL,
'return_url' => isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : admin_url(),
];
}
private function render() {
require __DIR__ . '/view.php';
}
/**
* Get Elementor UI theme preference.
*
* Retrieve the user UI theme preference as defined by editor preferences manager.
*
* @since 3.0.0
* @access private
*
* @return string Preferred UI theme.
*/
private function get_elementor_ui_theme_preference() {
$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );
return $editor_preferences->get_model()->get_settings( 'ui_theme' );
}
/**
* Enqueue dark theme detection script.
*
* Enqueues an inline script that detects user-agent settings for dark mode and adds a complimentary class to the body tag.
*
* @since 3.0.0
* @access private
*/
private function enqueue_dark_theme_detection_script() {
if ( 'auto' === $this->get_elementor_ui_theme_preference() ) {
wp_add_inline_script( 'elementor-app',
'if ( window.matchMedia && window.matchMedia( `(prefers-color-scheme: dark)` ).matches )
{ document.body.classList.add( `eps-theme-dark` ); }' );
}
}
private function enqueue_assets() {
Plugin::$instance->init_common();
Plugin::$instance->common->register_scripts();
wp_register_style(
'select2',
$this->get_css_assets_url( 'e-select2', 'assets/lib/e-select2/css/' ),
[],
'4.0.6-rc.1'
);
wp_register_style(
'elementor-icons',
$this->get_css_assets_url( 'elementor-icons', 'assets/lib/eicons/css/' ),
[],
'5.6.2'
);
wp_register_style(
'elementor-common',
$this->get_css_assets_url( 'common', null, 'default', true ),
[],
ELEMENTOR_VERSION
);
wp_enqueue_style(
'elementor-app',
$this->get_css_assets_url( 'app', null, 'default', true ),
[
'elementor-icons',
'elementor-common',
'select2',
],
ELEMENTOR_VERSION
);
wp_enqueue_script(
'elementor-app-packages',
$this->get_js_assets_url( 'app-packages' ),
[
'wp-i18n',
'react',
],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'select2',
$this->get_js_assets_url( 'e-select2.full', 'assets/lib/e-select2/js/' ),
[
'jquery',
],
'4.0.6-rc.1',
true
);
wp_enqueue_script(
'elementor-app',
$this->get_js_assets_url( 'app' ),
[
'wp-i18n',
'react',
'react-dom',
'select2',
],
ELEMENTOR_VERSION,
true
);
$this->enqueue_dark_theme_detection_script();
wp_set_script_translations( 'elementor-app-packages', 'elementor' );
wp_set_script_translations( 'elementor-app', 'elementor' );
$this->print_config();
}
public function enqueue_app_loader() {
wp_enqueue_script(
'elementor-app-loader',
$this->get_js_assets_url( 'app-loader' ),
[
'elementor-common',
],
ELEMENTOR_VERSION,
true
);
$this->print_config( 'elementor-app-loader' );
}
public function __construct() {
$this->add_component( 'site-editor', new Modules\SiteEditor\Module() );
add_action( 'admin_menu', [ $this, 'register_admin_menu' ], 21 /* after Elementor page */ );
// Happens after WP plugin page validation.
add_filter( 'add_menu_classes', [ $this, 'fix_submenu' ] );
if ( $this->is_current() ) {
add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
} else {
add_action( 'elementor/common/after_register_scripts', [ $this, 'enqueue_app_loader' ] );
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace Elementor\Core\App\Modules\SiteEditor;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Site Editor Module
*
* Responsible for initializing Elementor App functionality
*/
class Module extends BaseModule {
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'site-editor';
}
public function add_menu_in_admin_bar( $admin_bar_config ) {
$admin_bar_config['elementor_edit_page']['children'][] = [
'id' => 'elementor_app_site_editor',
'title' => __( 'Open Theme Builder', 'elementor' ),
'href' => Plugin::$instance->app->get_settings( 'menu_url' ),
'class' => 'elementor-app-link',
];
return $admin_bar_config;
}
public function __construct() {
add_filter( 'elementor/frontend/admin_bar/settings', [ $this, 'add_menu_in_admin_bar' ] );
}
}
@@ -0,0 +1,29 @@
<?php
namespace Elementor\Core\App;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @var App $this
*/
$theme_class = 'dark' === $this->get_elementor_ui_theme_preference() ? 'eps-theme-dark' : '';
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><?php echo __( 'Elementor', 'elementor' ) . ' ... '; ?></title>
<base target="_parent">
<?php wp_print_styles(); ?>
</head>
<body class="<?php echo $theme_class; ?>">
<div id="e-app"></div>
<?php wp_print_footer_scripts(); ?>
</body>
</html>
@@ -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;
}
}
@@ -0,0 +1,168 @@
<?php
namespace Elementor\Core\Base\BackgroundProcess;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 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: array())
*
* @var array
* @access protected
*/
protected $data = array();
/**
* Initiate new async request
*/
public function __construct() {
$this->identifier = $this->prefix . '_' . $this->action;
add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $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 array(
'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 array(
'timeout' => 0.01,
'blocking' => false,
'body' => $this->data,
'cookies' => $_COOKIE,
'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();
}
@@ -0,0 +1,517 @@
<?php
namespace Elementor\Core\Base\BackgroundProcess;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 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, array( $this, 'handle_cron_healthcheck' ) );
add_filter( 'cron_schedules', array( $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() . 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' ] = array(
'interval' => MINUTE_IN_SECONDS * $interval,
'display' => sprintf( __( '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 );
}
@@ -0,0 +1,89 @@
<?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;
}
// 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;
}
}
@@ -0,0 +1,382 @@
<?php
namespace Elementor\Core\Base;
use Elementor\Plugin;
use Elementor\Core\Base\BackgroundProcess\WP_Background_Process;
/**
* 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
*/
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( $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' ] = array(
'interval' => MINUTE_IN_SECONDS * $interval,
/* translators: %d: interval */
'display' => sprintf( __( '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();
}
}
@@ -0,0 +1,135 @@
<?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 = [];
}
}
/**
* 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();
}
}
}
@@ -0,0 +1,190 @@
<?php
namespace Elementor\Core\Base;
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();
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(),
],
] );
Plugin::$instance->files_manager->clear_cache();
$this->update_db_version();
if ( $did_tasks ) {
$this->add_flag( 'completed' );
}
}
public function admin_notice_start_upgrade() {
$upgrade_link = $this->get_start_action_url();
$message = '<p>' . sprintf( __( '%s Your site database needs to be updated to the latest version.', 'elementor' ), $this->get_updater_label() ) . '</p>';
$message .= '<p>' . sprintf( '<a href="%s" class="button-primary">%s</a>', $upgrade_link, __( 'Update Now', 'elementor' ) ) . '</p>';
echo '<div class="notice notice-error is-dismissible">' . $message . '</div>';
}
public function admin_notice_upgrade_is_running() {
$upgrade_link = $this->get_continue_action_url();
$message = '<p>' . sprintf( __( '%s Database update process is running in the background.', 'elementor' ), $this->get_updater_label() ) . '</p>';
$message .= '<p>' . __( 'Taking a while?', 'elementor' ) . ' <a href="' . $upgrade_link . '" class="button-primary">' . __( 'Click here to run it now', 'elementor' ) . '</a></p>';
echo '<div class="notice notice-warning is-dismissible">' . $message . '</div>';
}
public function admin_notice_upgrade_is_completed() {
$this->delete_flag( 'completed' );
$message = '<p>' . sprintf( __( '%s The database update process is now complete. Thank you for updating to the latest version!', 'elementor' ), $this->get_updater_label() ) . '</p>';
echo '<div class="notice notice-success is-dismissible">' . $message . '</div>';
}
/**
* @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;
}
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 ( 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
@@ -0,0 +1,300 @@
<?php
namespace Elementor\Core\Base;
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 Module 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();
}
/**
* 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() {
// Cloning instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, esc_html__( 'Something went wrong.', 'elementor' ), '1.0.0' );
}
/**
* Wakeup.
*
* Disable unserializing of the class.
*
* @since 1.7.0
* @access public
*/
public function __wakeup() {
// Unserializing instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, esc_html__( 'Something went wrong.', 'elementor' ), '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 = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || defined( 'ELEMENTOR_TESTS' ) && 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 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/';
}
}
@@ -0,0 +1,252 @@
<?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;
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' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
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() );
}
/**
* 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',
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.8.1',
true
);
wp_enqueue_script(
'elementor-common',
$this->get_js_assets_url( 'common' ),
[
'jquery',
'jquery-ui-draggable',
'backbone-marionette',
'backbone-radio',
'elementor-common-modules',
'elementor-dialog',
'wp-api-request',
],
ELEMENTOR_VERSION,
true
);
$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/' ),
[],
'5.9.1'
);
wp_enqueue_style(
'elementor-common',
$this->get_css_assets_url( 'common', null, 'default', true ),
[
'elementor-icons',
],
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;
}
}
/**
* 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() {
return [
'version' => ELEMENTOR_VERSION,
'isRTL' => is_rtl(),
'isDebug' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
'isElementorDebug' => ( defined( 'ELEMENTOR_DEBUG' ) && ELEMENTOR_DEBUG ),
'activeModules' => array_keys( $this->get_components() ),
'urls' => [
'assets' => ELEMENTOR_ASSETS_URL,
'rest' => get_rest_url(),
],
];
}
/**
* 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 );
}
}
}
@@ -0,0 +1,320 @@
<?php
namespace Elementor\Core\Common\Modules\Ajax;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Utils\Exceptions;
use Elementor\Plugin;
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, __( '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 );
$this->requests = json_decode( stripslashes( $_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, __( 'Action not found.', 'elementor' ), Exceptions::BAD_REQUEST );
continue;
}
if ( $editor_post_id ) {
$action_data['data']['editor_post_id'] = $editor_post_id;
}
try {
$results = call_user_func( $this->ajax_actions[ $action_data['action'] ]['callback'], $action_data['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 ! empty( $_REQUEST['_nonce'] ) && wp_verify_nonce( $_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();
}
if ( function_exists( 'gzencode' ) ) {
$response = gzencode( $json );
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Encoding: gzip' );
header( 'Content-Length: ' . strlen( $response ) );
echo $response;
} else {
echo $json;
}
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;
}
}
@@ -0,0 +1,109 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Plugin;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin {
const PAGE_ID = 'elementor-connect';
public static $url = '';
/**
* @since 2.3.0
* @access public
*/
public function register_admin_menu() {
$submenu_page = add_submenu_page(
Settings::PAGE_ID,
__( 'Connect', 'elementor' ),
__( 'Connect', 'elementor' ),
'edit_posts',
self::PAGE_ID,
[ $this, 'render_page' ]
);
add_action( 'load-' . $submenu_page, [ $this, 'on_load_page' ] );
}
/**
* @since 2.3.0
* @access public
*/
public function hide_menu_item() {
remove_submenu_page( Settings::PAGE_ID, self::PAGE_ID );
}
/**
* @since 2.3.0
* @access public
*/
public function on_load_page() {
if ( isset( $_GET['action'], $_GET['app'] ) ) {
$manager = Plugin::$instance->common->get_component( 'connect' );
$app_slug = $_GET['app'];
$app = $manager->get_app( $app_slug );
$nonce_action = $_GET['app'] . $_GET['action'];
if ( ! $app ) {
wp_die( 'Unknown app: ' . esc_attr( $app_slug ) );
}
if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], $nonce_action ) ) {
wp_die( 'Invalid Nonce', 'Invalid Nonce', [
'back_link' => true,
] );
}
$method = 'action_' . $_GET['action'];
if ( method_exists( $app, $method ) ) {
call_user_func( [ $app, $method ] );
}
}
}
/**
* @since 2.3.0
* @access public
*/
public function render_page() {
$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 \Elementor\Core\Common\Modules\Connect\Apps\Base_App $app */
foreach ( $apps as $app ) {
echo '<div class="elementor-connect-app-wrapper">';
$app->render_admin_widget();
echo '</div>';
}
?>
</div><!-- /.wrap -->
<?php
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
self::$url = admin_url( 'admin.php?page=' . self::PAGE_ID );
add_action( 'admin_menu', [ $this, 'register_admin_menu' ], 206 );
add_action( 'admin_head', [ $this, 'hide_menu_item' ] );
}
}
@@ -0,0 +1,629 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\Core\Common\Modules\Connect\Admin;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Base_App {
const OPTION_NAME_PREFIX = 'elementor_connect_';
const SITE_URL = 'https://my.elementor.com/connect/v1';
const API_URL = 'https://my.elementor.com/api/connect/v1';
protected $data = [];
protected $auth_mode = '';
/**
* @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() {
echo '<h2>' . $this->get_title() . '</h2>';
if ( $this->is_connected() ) {
$remote_user = $this->get( 'user' );
$title = sprintf( __( 'Connected as %s', 'elementor' ), '<strong>' . $remote_user->email . '</strong>' );
$label = __( 'Disconnect', 'elementor' );
$url = $this->get_admin_url( 'disconnect' );
$attr = '';
echo sprintf( '%s <a %s href="%s">%s</a>', $title, $attr, 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>', $this->get_admin_url( 'reset' ), __( '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 ) ) {
wp_die( $response, $response->get_error_message() );
}
// 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( __( '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() {
delete_user_option( get_current_user_id(), 'elementor_connect_common_data' );
if ( current_user_can( 'manage_options' ) ) {
delete_option( 'elementor_connect_site_key' );
delete_option( 'elementor_remote_info_library' );
}
$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();
}
if ( empty( $_REQUEST['state'] ) || $_REQUEST['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' => $_REQUEST['code'],
'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();
}
if ( ! empty( $response->data_share_opted_in ) && current_user_can( 'manage_options' ) ) {
Tracker::set_opt_in( true );
}
$this->delete( 'state' );
$this->set( (array) $response );
$this->after_connect();
// Add the notice *after* the method `after_connect`, so an app can redirect without the notice.
$this->add_notice( __( '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( __( '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;
// Encode base url, the encode is limited to 64 chars.
$admin_url = \Requests_IDNAEncoder::encode( 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 = null ) {
$this->init_data();
return isset( $this->data[ $key ] ) ? $this->data[ $key ] : $default;
}
/**
* @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 = '' ) {
$new_value = $this->get( $key, $default );
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' ), [] );
}
/**
* @since 2.3.0
* @access protected
*/
protected function request( $action, $request_body = [], $as_array = false ) {
$request_body = [
'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() ),
] + $request_body;
$headers = [];
if ( $this->is_connected() ) {
$headers['X-Elementor-Signature'] = hash_hmac( 'sha256', wp_json_encode( $request_body, JSON_NUMERIC_CHECK ), $this->get( 'access_token_secret' ) );
}
$response = wp_remote_post( $this->get_api_url() . '/' . $action, [
'body' => $request_body,
'headers' => $headers,
'timeout' => 25,
] );
if ( is_wp_error( $response ) ) {
wp_die( $response, [
'back_link' => true,
] );
}
$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, $as_array );
if ( false === $body ) {
return new \WP_Error( 422, 'Wrong Server Response' );
}
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 = isset( $body->code ) ? $body->code : $response_code;
if ( 401 === $code ) {
$this->delete();
$this->action_authorize();
}
return new \WP_Error( $code, $message );
}
return $body;
}
/**
* @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();
$url = add_query_arg( [
'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' ),
], $this->get_remote_site_url() );
return $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':
$this->admin_notice();
die;
default:
wp_safe_redirect( $url );
die;
}
}
/**
* @since 2.3.0
* @access protected
*/
protected function set_client_id() {
if ( $this->get( 'client_id' ) ) {
return;
}
$response = $this->request( 'get_client_id' );
if ( is_wp_error( $response ) ) {
wp_die( $response, $response->get_error_message() );
}
$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 ) );
}
/**
* @since 2.3.0
* @access protected
*/
protected function print_popup_close_script( $url ) {
?>
<script>
if ( opener && opener !== window ) {
opener.jQuery( 'body' ).trigger( 'elementor/connect/success/<?php echo esc_attr( $_REQUEST['callback_id'] ); ?>' );
window.close();
opener.focus();
} else {
location = '<?php echo $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
*/
protected function get_site_key() {
$site_key = get_option( 'elementor_connect_site_key' );
if ( ! $site_key ) {
$site_key = md5( uniqid( wp_generate_password() ) );
update_option( 'elementor_connect_site_key', $site_key );
}
return $site_key;
}
protected function redirect_to_remote_authorize_url() {
switch ( $this->auth_mode ) {
case 'cli':
$this->get_app_token_from_cli_token( $_REQUEST['token'] );
return;
default:
wp_redirect( $this->get_remote_authorize_url() );
die;
}
}
protected function get_auth_redirect_uri() {
$redirect_uri = $this->get_admin_url( 'get_token' );
switch ( $this->auth_mode ) {
case 'popup':
$redirect_uri = add_query_arg( [
'mode' => 'popup',
'callback_id' => esc_attr( $_REQUEST['callback_id'] ),
], $redirect_uri );
break;
}
return $redirect_uri;
}
protected function print_notices( $notices ) {
switch ( $this->auth_mode ) {
case 'cli':
foreach ( $notices as $notice ) {
printf( '[%s] %s', $notice['type'], $notice['content'] );
}
break;
default:
echo '<div id="message" class="updated notice is-dismissible"><p>';
foreach ( $notices as $notice ) {
echo wp_kses_post( sprintf( '<div class="%s"><p>%s</p></div>', $notice['type'], wpautop( $notice['content'] ) ) );
}
echo '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">' . __( 'Dismiss', 'elementor' ) . '</span></button></div>';
}
}
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';
}
printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status );
}
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
add_action( 'admin_notices', [ $this, 'admin_notice' ] );
if ( isset( $_REQUEST['mode'] ) ) { // phpcs:ignore -- nonce validation is not require here.
$allowed_auth_modes = [
'popup',
];
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$allowed_auth_modes[] = 'cli';
}
$mode = $_REQUEST['mode']; // phpcs:ignore -- nonce validation is not require here.
if ( in_array( $mode, $allowed_auth_modes, true ) ) {
$this->auth_mode = $mode;
}
}
/**
* Allow extended apps to customize the __construct without call parent::__construct.
*/
$this->init();
}
}
@@ -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 = [];
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Common_App extends Base_User_App {
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;
}
}
@@ -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 __( '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() {}
}
@@ -0,0 +1,103 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Library extends Common_App {
public function get_title() {
return __( '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', __( '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 );
return $template_content;
}
public function localize_settings( $settings ) {
$is_connected = $this->is_connected();
return array_replace_recursive( $settings, [
'i18n' => [
// Route: library/connect
'library/connect:title' => __( 'Connect to Template Library', 'elementor' ),
'library/connect:message' => __( 'Access this template and our entire library by creating a free personal account', 'elementor' ),
'library/connect:button' => __( 'Get Started', 'elementor' ),
],
'library_connect' => [
'is_connected' => $is_connected,
],
] );
}
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' ] );
}
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( 'elementor_connect_site_key' ),
],
'remote_info_library' => [
'label' => 'Remote Library Info',
'value' => get_option( 'elementor_remote_info_library' ),
],
];
}
protected function init() {
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
}
@@ -0,0 +1,173 @@
<?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\Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
/**
* @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(),
];
// Note: The priority 11 is for allowing plugins to add their register callback on elementor init.
add_action( 'elementor/init', [ $this, 'init' ], 11 );
}
/**
* 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();
}
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
}
public function localize_settings( $settings ) {
return array_replace_recursive( $settings, [
'i18n' => [
'connect_error' => __( 'Unable to connect', 'elementor' ),
'connected_successfully' => __( 'Connected successfully', 'elementor' ),
],
] );
}
/**
* Register app.
*
* Registers an app.
*
* @since 2.3.0
* @access public
*
* @param string $slug App slug.
* @param string $class App full class name.
*
* @return self The updated apps manager instance.
*/
public function register_app( $slug, $class ) {
$this->registered_apps[ $slug ] = $class;
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;
}
}
@@ -0,0 +1,76 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Core\Base\Base_Object;
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 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;
}
}
@@ -0,0 +1,94 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
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
* @access public
* @param string $category_name
* @param Base_Category $category
*/
public function add_category( $category_name, Base_Category $category ) {
$this->categories[ $category_name ] = $category;
}
/**
* Get categories.
*
* @since 2.3.0
* @access public
* @param string $category
*
* @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 finder default 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->add_category( $category_name, 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
*
* @param Categories_Manager $this.
*/
do_action( 'elementor/finder/categories/init', $this );
}
}
@@ -0,0 +1,71 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
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 __( 'Create', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$elementor_supported_post_types = get_post_types_by_support( 'elementor' );
$items = [];
foreach ( $elementor_supported_post_types as $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 ) {
continue;
}
if ( Source_Local::CPT === $post_type ) {
$url = admin_url( Source_Local::ADMIN_MENU_SLUG . '#add_new' );
} else {
$url = Utils::get_create_new_post_url( $post_type );
}
$items[ $post_type ] = [
/* translators: %s the title of the post type */
'title' => sprintf( __( 'Add New %s', 'elementor' ), $post_type_object->labels->singular_name ),
'icon' => 'plus-circle-o',
'url' => $url,
'keywords' => [ 'post', 'page', 'template', 'new', 'create' ],
];
}
return $items;
}
}
@@ -0,0 +1,138 @@
<?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 __( 'Edit', 'elementor' );
}
/**
* 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,
] );
// TODO: Remove on 2.4.0.
unset( $document_types['widget'] );
$recently_edited_query_args = [
'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 = __( '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;
}
}
@@ -0,0 +1,68 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Core\RoleManager\Role_Manager;
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 __( 'General', 'elementor' );
}
/**
* 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' => _x( 'Saved Templates', 'Template Library', 'elementor' ),
'icon' => 'library-save',
'url' => Source_Local::get_admin_url(),
'keywords' => [ 'template', 'section', 'page', 'library' ],
],
'system-info' => [
'title' => __( 'System Info', 'elementor' ),
'icon' => 'info-circle-o',
'url' => admin_url( 'admin.php?page=elementor-system-info' ),
'keywords' => [ 'system', 'info', 'environment', 'elementor' ],
],
'role-manager' => [
'title' => __( 'Role Manager', 'elementor' ),
'icon' => 'person',
'url' => Role_Manager::get_url(),
'keywords' => [ 'role', 'manager', 'user', 'elementor' ],
],
'knowledge-base' => [
'title' => __( 'Knowledge Base', 'elementor' ),
'url' => admin_url( 'admin.php?page=go_knowledge_base_site' ),
'keywords' => [ 'help', 'knowledge', 'docs', 'elementor' ],
],
];
}
}
@@ -0,0 +1,57 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
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 __( 'Settings', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$settings_url = ElementorSettings::get_url();
return [
'general-settings' => [
'title' => __( 'General Settings', 'elementor' ),
'url' => $settings_url,
'keywords' => [ 'general', 'settings', 'elementor' ],
],
'advanced' => [
'title' => __( 'Advanced', 'elementor' ),
'url' => $settings_url . '#tab-advanced',
'keywords' => [ 'advanced', 'settings', 'elementor' ],
],
];
}
}
@@ -0,0 +1,85 @@
<?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 __( 'Site', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'homepage' => [
'title' => __( 'Homepage', 'elementor' ),
'url' => home_url(),
'icon' => 'home-heart',
'keywords' => [ 'home', 'page' ],
],
'wordpress-dashboard' => [
'title' => __( 'Dashboard', 'elementor' ),
'icon' => 'dashboard',
'url' => admin_url(),
'keywords' => [ 'dashboard', 'wordpress' ],
],
'wordpress-menus' => [
'title' => __( 'Menus', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'nav-menus.php' ),
'keywords' => [ 'menu', 'wordpress' ],
],
'wordpress-themes' => [
'title' => __( 'Themes', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'themes.php' ),
'keywords' => [ 'themes', 'wordpress' ],
],
'wordpress-customizer' => [
'title' => __( 'Customizer', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'customize.php' ),
'keywords' => [ 'customizer', 'wordpress' ],
],
'wordpress-plugins' => [
'title' => __( 'Plugins', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'plugins.php' ),
'keywords' => [ 'plugins', 'wordpress' ],
],
'wordpress-users' => [
'title' => __( 'Users', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'users.php' ),
'keywords' => [ 'users', 'profile', 'wordpress' ],
],
];
}
}
@@ -0,0 +1,71 @@
<?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 __( 'Tools', 'elementor' );
}
/**
* 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();
return [
'tools' => [
'title' => __( 'Tools', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url,
'keywords' => [ 'tools', 'regenerate css', 'safe mode', 'debug bar', 'sync library', 'elementor' ],
],
'replace-url' => [
'title' => __( 'Replace URL', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-replace_url',
'keywords' => [ 'tools', 'replace url', 'domain', 'elementor' ],
],
'version-control' => [
'title' => __( 'Version Control', 'elementor' ),
'icon' => 'time-line',
'url' => $tools_url . '#tab-versions',
'keywords' => [ 'tools', 'version', 'control', 'rollback', 'beta', 'elementor' ],
],
'maintenance-mode' => [
'title' => __( 'Maintenance Mode', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-maintenance_mode',
'keywords' => [ 'tools', 'maintenance', 'coming soon', 'elementor' ],
],
];
}
}
@@ -0,0 +1,119 @@
<?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
*/
public function ajax_get_category_items( array $data ) {
$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 ] );
}
$categories_data = apply_filters( 'elementor/finder/categories', $categories_data );
return [
'data' => $categories_data,
'i18n' => [
'finder' => __( 'Finder', 'elementor' ),
],
];
}
}
@@ -0,0 +1,47 @@
<?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"></i>
<input id="elementor-finder__search__input" placeholder="<?php echo __( 'Type to find anything in Elementor', 'elementor' ); ?>">
</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 __( '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 }}}"></i>
</div>
<div class="elementor-finder__results__item__title">{{{ title }}}</div>
<# if ( description ) { #>
<div class="elementor-finder__results__item__description">- {{{ description }}}</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>
@@ -0,0 +1,46 @@
<?php
namespace Elementor\Core\Debug\Classes;
use Elementor\Modules\SafeMode\Module as Safe_Mode;
class Htaccess extends Inspection_Base {
private $message = '';
public function __construct() {
$this->message = __( '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( $_SERVER['SERVER_SOFTWARE'] );
if ( strstr( $server, 'APACHE' ) ) {
$htaccess_file = get_home_path() . '.htaccess';
$this->message .= ' ' . sprintf( __( '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';
}
}
@@ -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 __( 'The preview could not be loaded', 'elementor' );
}
/**
* @return string
*/
abstract public function get_help_doc_url();
}
@@ -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 __( 'Some of your theme files are missing.', 'elementor' );
}
public function get_help_doc_url() {
return 'https://go.elementor.com/preview-not-loaded/#theme-files';
}
}
@@ -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' => __( 'Debug Bar', 'elementor' ),
'field_args' => [
'type' => 'select',
'std' => $this->is_enabled ? 'enable' : '',
'options' => [
'' => __( 'Disable', 'elementor' ),
'enable' => __( 'Enable', 'elementor' ),
],
'desc' => __( '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' => __( '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',
],
] );
}
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace Elementor\Core\Debug;
use Elementor\Core\Debug\Classes\Inspection_Base;
use Elementor\Core\Debug\Classes\Theme_Missing;
use Elementor\Core\Debug\Classes\Htaccess;
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();
}
/**
* @param Inspection_Base $inspection
*/
public function register_inspection( $inspection ) {
$this->inspections[ $inspection->get_name() ] = $inspection;
}
public function run_inspections() {
$debug_data = [
'message' => __( 'We\'re sorry, but something went wrong. Click on \'Learn more\' and follow each of the steps to quickly solve it.', 'elementor' ),
'header' => __( '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;
}
}
@@ -0,0 +1,219 @@
<?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 {
/**
* @since 2.0.8
* @access public
* @static
*/
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' => __( 'Single', 'elementor' ),
'active' => false,
],
]
);
}
/**
* @since 2.0.0
* @access public
*/
public function get_css_wrapper_selector() {
return 'body.elementor-page-' . $this->get_main_id();
}
/**
* @since 2.0.0
* @access protected
*/
protected function _register_controls() {
parent::_register_controls();
self::register_hide_title_control( $this );
self::register_post_fields_control( $this );
self::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' => __( 'Hide Title', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'description' => __( 'Not working? You can set a different selector for the title in Site Settings > Layout', 'elementor' ),
'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' => __( 'Body Style', 'elementor' ),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$document->add_group_control(
Group_Control_Background::get_type(),
[
'name' => 'background',
'fields_options' => [
'image' => [
// Currently isn't supported.
'dynamic' => [
'active' => false,
],
],
],
]
);
$document->add_responsive_control(
'padding',
[
'label' => __( 'Padding', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', 'em', '%' ],
'selectors' => [
'{{WRAPPER}}' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}',
],
]
);
$document->end_controls_section();
Plugin::$instance->controls_manager->add_custom_css_controls( $document );
}
/**
* @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' => __( 'Excerpt', 'elementor' ),
'type' => Controls_Manager::TEXTAREA,
'default' => $document->post->post_excerpt,
]
);
}
if ( current_theme_supports( 'post-thumbnails' ) && post_type_supports( $document->post->post_type, 'thumbnail' ) ) {
$document->add_control(
'post_featured_image',
[
'label' => __( 'Featured Image', 'elementor' ),
'type' => Controls_Manager::MEDIA,
'default' => [
'id' => get_post_thumbnail_id(),
'url' => (string) get_the_post_thumbnail_url( $document->post->ID ),
],
]
);
}
$document->end_injection();
}
/**
* @since 2.0.0
* @access public
*
* @param array $data
*
* @throws \Exception
*/
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'] = 'page';
$config['default_route'] = 'templates/pages';
return $config;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Elementor\Core\DocumentTypes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Page extends PageBase {
public static function get_properties() {
$properties = parent::get_properties();
$properties['cpt'] = [ 'page' ];
$properties['support_kit'] = true;
return $properties;
}
/**
* @access public
*/
public function get_name() {
return 'wp-page';
}
/**
* @access public
* @static
*/
public static function get_title() {
return __( 'Page', 'elementor' );
}
}
@@ -0,0 +1,32 @@
<?php
namespace Elementor\Core\DocumentTypes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Post extends PageBase {
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
return $properties;
}
/**
* @access public
*/
public function get_name() {
return 'wp-post';
}
/**
* @access public
* @static
*/
public static function get_title() {
return __( 'Post', 'elementor' );
}
}
@@ -0,0 +1,696 @@
<?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\DB;
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' ] );
}
/**
* 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 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 ) {
$this->types[ $type ] = $class;
$cpt = $class::get_property( 'cpt' );
if ( $cpt ) {
foreach ( $cpt as $post_type ) {
$this->cpt[ $post_type ] = $type;
}
}
if ( $class::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;
}
$post_id = apply_filters( 'elementor/documents/get/post_id', $post_id );
if ( ! $from_cache || ! isset( $this->documents[ $post_id ] ) ) {
if ( wp_is_post_autosave( $post_id ) ) {
$post_type = get_post_type( wp_get_post_parent_id( $post_id ) );
} else {
$post_type = get_post_type( $post_id );
}
$doc_type = 'post';
if ( isset( $this->cpt[ $post_type ] ) ) {
$doc_type = $this->cpt[ $post_type ];
}
$meta_type = get_post_meta( $post_id, Document::TYPE_META_KEY, true );
if ( $meta_type && isset( $this->types[ $meta_type ] ) ) {
$doc_type = $meta_type;
}
$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 ];
}
/**
* 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 ) {
if ( is_preview() || 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'] = __( '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_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 $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 = DB::STATUS_DRAFT;
if ( isset( $request['status'] ) && in_array( $request['status'], [ DB::STATUS_PUBLISH, DB::STATUS_PRIVATE, DB::STATUS_PENDING, DB::STATUS_AUTOSAVE ], true ) ) {
$status = $request['status'];
}
if ( DB::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, [ DB::STATUS_PUBLISH, DB::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 );
// Refresh after save.
$document = $this->get( $document->get_post()->ID, false );
$return_data = [
'status' => $document->get_post()->post_status,
'config' => [
'document' => [
'last_edited' => $document->get_last_edited(),
'urls' => [
'wp_preview' => $document->get_wp_preview_url(),
],
],
],
];
/**
* 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.
*
* @since 2.0.0
* @access public
*
* @param $request
*
* @return bool True if changes discarded, False otherwise.
*/
public function ajax_discard_changes( $request ) {
$document = $this->get( $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
Plugin::$instance->db->set_is_elementor_page( $post_id );
$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;
}
/**
* Get groups.
*
* @since 2.0.0
* @deprecated 2.4.0
* @access public
*
* @return array
*/
public function get_groups() {
_deprecated_function( __METHOD__, '2.4.0' );
return [];
}
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 );
}
}
}
@@ -0,0 +1,185 @@
<?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();
/**
* @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(),
'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( '(' + 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' => __( 'Settings', 'elementor' ),
] );
$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();
}
}
@@ -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 );
}
}
@@ -0,0 +1,112 @@
<?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() {
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';
}
/**
* @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 );
}
}
}
}
@@ -0,0 +1,458 @@
<?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__';
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();
}
/**
* Localize settings.
*
* Add new localized settings for the dynamic module.
*
* Fired by `elementor/editor/localize_settings` filter.
*
* @access public
*
* @param array $settings Localized settings.
*
* @return array Localized settings.
*/
public function localize_settings( $settings ) {
$settings = array_replace_recursive( $settings, [
'i18n' => [
'dynamic' => __( 'Dynamic', 'elementor' ),
],
] );
return $settings;
}
/**
* 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, $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 );
}
/**
* @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' => $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, $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
*
* @param Manager $this Dynamic tags manager.
*/
do_action( 'elementor/dynamic_tags/register_tags', $this );
}
return $this->tags_info;
}
/**
* @since 2.0.0
* @access public
*
* @param string $class
*/
public function register_tag( $class ) {
/** @var Tag $tag */
$tag = new $class();
$this->tags_info[ $tag->get_name() ] = [
'class' => $class,
'instance' => $tag,
];
}
/**
* @since 2.0.9
* @access public
*
* @param string $tag_name
*/
public function unregister_tag( $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.
* @throws \Exception If 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 ) {
$css_file = Dynamic_CSS::create( $css_file->get_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' ] );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
}
}
@@ -0,0 +1,115 @@
<?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 = $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' => __( 'Advanced', 'elementor' ),
]
);
$this->add_control(
'before',
[
'label' => __( 'Before', 'elementor' ),
]
);
$this->add_control(
'after',
[
'label' => __( 'After', 'elementor' ),
]
);
$this->add_control(
'fallback',
[
'label' => __( 'Fallback', 'elementor' ),
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,29 @@
<?php
namespace Elementor\Core\Editor\Data\Globals;
use Elementor\Data\Base\Controller as Controller_Base;
use Elementor\Plugin;
class Controller extends Controller_Base {
public function get_name() {
return 'globals';
}
public function register_endpoints() {
$this->register_endpoint( Endpoints\Colors::class );
$this->register_endpoint( Endpoints\Typography::class );
}
protected function register_internal_endpoints() {
$this->register_endpoint( Endpoints\Index::class );
}
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->is_internal() ) {
return true;
}
return current_user_can( 'edit_posts' );
}
}
@@ -0,0 +1,64 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Data\Base\Endpoint;
use Elementor\Plugin;
use Elementor\Core\Utils\Exceptions;
abstract class Base extends Endpoint {
public static function get_format() {
return '{id}';
}
protected function register() {
parent::register();
$this->register_item_route();
$this->register_item_route( \WP_REST_Server::CREATABLE );
$this->register_item_route( \WP_REST_Server::DELETABLE );
}
public function get_items( $request ) {
return $this->get_kit_items();
}
public function get_item( $id, $request ) {
$items = $this->get_kit_items();
if ( ! isset( $items[ $id ] ) ) {
return new \WP_Error(
'global_not_found',
__( 'Global value was not found.', 'elementor' ),
[ 'status' => Exceptions::NOT_FOUND ]
);
}
return $items[ $id ];
}
public function create_item( $id, $request ) {
$item = $request->get_body_params();
if ( ! isset( $item['title'] ) ) {
return new \WP_Error( 'invalid_title', '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 );
}
@@ -0,0 +1,47 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Plugin;
class Colors extends Base {
public function get_name() {
return 'colors';
}
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' => $item['title'],
'color' => $item['value'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Data\Base\Endpoint;
// TODO: Create base class for index endpoints, and move this function to there.
class Index extends Endpoint {
public function get_name() {
return 'index'; // TODO: Replace with BaseIndex class.
}
public function get_items( $request ) {
return $this->controller->get_items_recursive( [ $this ] ); // TODO: Replace with BaseIndex class.
}
}
@@ -0,0 +1,69 @@
<?php
namespace Elementor\Core\Editor\Data\Globals\Endpoints;
use Elementor\Plugin;
class Typography extends Base {
public function get_name() {
return 'typography';
}
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' => $item['title'],
];
$db_format = array_merge( $item['value'], $db_format );
return $db_format;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
<?php
namespace Elementor\Core\Editor;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Notice_Bar extends Base_Object {
protected function get_init_settings() {
if ( Plugin::$instance->get_install_time() > strtotime( '-30 days' ) ) {
return [];
}
return [
'muted_period' => 90,
'option_key' => '_elementor_editor_upgrade_notice_dismissed',
'message' => __( 'Love using Elementor? <a href="%s">Learn how you can build better sites with Elementor Pro.</a>', 'elementor' ),
'action_title' => __( 'Go Pro', 'elementor' ),
'action_url' => Utils::get_pro_link( 'https://elementor.com/pro/?utm_source=editor-notice-bar&utm_campaign=gopro&utm_medium=wp-dash' ),
];
}
final public function get_notice() {
if ( ! current_user_can( 'manage_options' ) ) {
return null;
}
$settings = $this->get_settings();
if ( empty( $settings['option_key'] ) ) {
return null;
}
$dismissed_time = get_option( $settings['option_key'] );
if ( $dismissed_time ) {
if ( $dismissed_time > strtotime( '-' . $settings['muted_period'] . ' days' ) ) {
return null;
}
$this->set_notice_dismissed();
}
return $settings;
}
public function __construct() {
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
public function set_notice_dismissed() {
update_option( $this->get_settings( 'option_key' ), time() );
}
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'notice_bar_dismiss', [ $this, 'set_notice_dismissed' ] );
}
}
@@ -0,0 +1,131 @@
<?php
namespace Elementor\Core\Files\Assets;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Files_Upload_Handler {
const OPTION_KEY = 'elementor_unfiltered_files_upload';
public function __construct() {
add_filter( 'upload_mimes', [ $this, 'support_unfiltered_files_upload' ] );
add_filter( 'wp_handle_upload_prefilter', [ $this, 'handle_upload_prefilter' ] );
add_filter( 'wp_check_filetype_and_ext', [ $this, 'check_filetype_and_ext' ], 10, 4 );
}
abstract public function get_mime_type();
abstract public function get_file_type();
/**
* is_elementor_media_upload
* @return bool
*/
private function is_elementor_media_upload() {
return isset( $_POST['uploadTypeCaller'] ) && 'elementor-editor-upload' === $_POST['uploadTypeCaller']; // phpcs:ignore
}
/**
* @return bool
*/
final public static function is_enabled() {
$enabled = ! ! get_option( self::OPTION_KEY ) && self::file_sanitizer_can_run();
/**
* @deprecated 3.0.0 Use `elementor/document/urls/edit` filter instead.
*/
$enabled = apply_filters( 'elementor/files/svg/enabled', $enabled );
/**
* Allow Unfiltered Files Upload.
*
* Determines whether to enable unfiltered file uploads.
*
* @since 3.0.0
*
* @param bool $enabled Weather upload is enabled or not.
*/
$enabled = apply_filters( 'elementor/files/allow_unfiltered_upload', $enabled );
return $enabled;
}
final public function support_unfiltered_files_upload( $existing_mimes ) {
$existing_mimes[ $this->get_file_type() ] = $this->get_mime_type();
return $existing_mimes;
}
/**
* handle_upload_prefilter
* @param $file
*
* @return mixed
*/
public function handle_upload_prefilter( $file ) {
if ( ! $this->is_file_should_handled( $file ) ) {
return $file;
}
$ext = pathinfo( $file['name'], PATHINFO_EXTENSION );
$file_type = $this->get_file_type();
$display_type = strtoupper( $file_type );
if ( $file_type !== $ext ) {
$file['error'] = sprintf( __( 'The uploaded %1$s file is not supported. Please upload a valid %2$s file', 'elementor' ), $ext, $display_type );
return $file;
}
if ( ! self::is_enabled() ) {
$file['error'] = sprintf( __( '%1$s file is not allowed for security reasons', 'elementor' ), $display_type );
return $file;
}
return $file;
}
protected function is_file_should_handled( $file ) {
return $this->is_elementor_media_upload() && $this->get_mime_type() === $file['type'];
}
/**
* file_sanitizer_can_run
* @return bool
*/
public static function file_sanitizer_can_run() {
return class_exists( 'DOMDocument' ) && class_exists( 'SimpleXMLElement' );
}
/**
* Check filetype and ext
*
* A workaround for upload validation which relies on a PHP extension (fileinfo)
* with inconsistent reporting behaviour.
* ref: https://core.trac.wordpress.org/ticket/39550
* ref: https://core.trac.wordpress.org/ticket/40175
*
* @param $data
* @param $file
* @param $filename
* @param $mimes
*
* @return mixed
*/
public function check_filetype_and_ext( $data, $file, $filename, $mimes ) {
if ( ! empty( $data['ext'] ) && ! empty( $data['type'] ) ) {
return $data;
}
$wp_file_type = wp_check_filetype( $filename, $mimes );
$file_type = strtolower( $this->get_file_type() );
if ( $file_type === $wp_file_type['ext'] ) {
$data['ext'] = $file_type;
$data['type'] = $this->get_mime_type();
}
return $data;
}
}
@@ -0,0 +1,22 @@
<?php
namespace Elementor\Core\Files\Assets\Json;
use Elementor\Core\Files\Assets\Files_Upload_Handler;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Json_Handler extends Files_Upload_Handler {
public static function get_name() {
return 'json-handler';
}
public function get_mime_type() {
return 'application/json';
}
public function get_file_type() {
return 'json';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Elementor\Core\Files\Assets;
use Elementor\Core\Files\Assets\Svg\Svg_Handler;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor files manager.
*
* Elementor files manager handler class is responsible for creating files.
*
* @since 2.6.0
*/
class Manager {
/**
* Holds registered asset types
* @var array
*/
protected $asset_types = [];
/**
* Assets manager constructor.
*
* Initializing the Elementor assets manager.
*
* @access public
*/
public function __construct() {
$this->register_asset_types();
/**
* Elementor files assets registered.
*
* Fires after Elementor registers assets types
*
* @since 2.6.0
*/
do_action( 'elementor/core/files/assets/assets_registered', $this );
}
public function get_asset( $name ) {
return isset( $this->asset_types[ $name ] ) ? $this->asset_types[ $name ] : false;
}
/**
* Add Asset
* @param $instance
*/
public function add_asset( $instance ) {
$this->asset_types[ $instance::get_name() ] = $instance;
}
/**
* Register Asset Types
*
* Registers Elementor Asset Types
*/
private function register_asset_types() {
$this->add_asset( new Svg_Handler() );
}
}
@@ -0,0 +1,704 @@
<?php
namespace Elementor\Core\Files\Assets\Svg;
use Elementor\Core\Files\Assets\Files_Upload_Handler;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Svg_Handler extends Files_Upload_Handler {
/**
* Inline svg attachment meta key
*/
const META_KEY = '_elementor_inline_svg';
const SCRIPT_REGEX = '/(?:\w+script|data):/xi';
/**
* @var \DOMDocument
*/
private $svg_dom = null;
/**
* Attachment ID.
*
* Holds the current attachment ID.
*
* @var int
*/
private $attachment_id;
public static function get_name() {
return 'svg-handler';
}
/**
* get_meta
* @return mixed
*/
protected function get_meta() {
return get_post_meta( $this->attachment_id, self::META_KEY, true );
}
/**
* update_meta
* @param $meta
*/
protected function update_meta( $meta ) {
update_post_meta( $this->attachment_id, self::META_KEY, $meta );
}
/**
* delete_meta
*/
protected function delete_meta() {
delete_post_meta( $this->attachment_id, self::META_KEY );
}
public function get_mime_type() {
return 'image/svg+xml';
}
public function get_file_type() {
return 'svg';
}
/**
* delete_meta_cache
*/
public function delete_meta_cache() {
delete_post_meta_by_key( self::META_KEY );
}
/**
* read_from_file
* @return bool|string
*/
public function read_from_file() {
return file_get_contents( get_attached_file( $this->attachment_id ) );
}
/**
* get_inline_svg
* @param $attachment_id
*
* @return bool|mixed|string
*/
public static function get_inline_svg( $attachment_id ) {
$svg = get_post_meta( $attachment_id, self::META_KEY, true );
if ( ! empty( $svg ) ) {
return $svg;
}
$attachment_file = get_attached_file( $attachment_id );
if ( ! $attachment_file ) {
return '';
}
$svg = file_get_contents( $attachment_file );
if ( ! empty( $svg ) ) {
update_post_meta( $attachment_id, self::META_KEY, $svg );
}
return $svg;
}
/**
* decode_svg
* @param $content
*
* @return string
*/
private function decode_svg( $content ) {
return gzdecode( $content );
}
/**
* encode_svg
* @param $content
*
* @return string
*/
private function encode_svg( $content ) {
return gzencode( $content );
}
/**
* sanitize_svg
* @param $filename
*
* @return bool
*/
public function sanitize_svg( $filename ) {
$original_content = file_get_contents( $filename );
$is_encoded = $this->is_encoded( $original_content );
if ( $is_encoded ) {
$decoded = $this->decode_svg( $original_content );
if ( false === $decoded ) {
return false;
}
$original_content = $decoded;
}
$valid_svg = $this->sanitizer( $original_content );
if ( false === $valid_svg ) {
return false;
}
// If we were gzipped, we need to re-zip
if ( $is_encoded ) {
$valid_svg = $this->encode_svg( $valid_svg );
}
file_put_contents( $filename, $valid_svg );
return true;
}
/**
* Check if the contents are gzipped
* @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
*
* @param $contents
* @return bool
*/
private function is_encoded( $contents ) {
$needle = "\x1f\x8b\x08";
if ( function_exists( 'mb_strpos' ) ) {
return 0 === mb_strpos( $contents, $needle );
} else {
return 0 === strpos( $contents, $needle );
}
}
/**
* is_allowed_tag
* @param $element
*
* @return bool
*/
private function is_allowed_tag( $element ) {
static $allowed_tags = false;
if ( false === $allowed_tags ) {
$allowed_tags = $this->get_allowed_elements();
}
$tag_name = $element->tagName; // phpcs:ignore -- php DomDocument
if ( ! in_array( strtolower( $tag_name ), $allowed_tags ) ) {
$this->remove_element( $element );
return false;
}
return true;
}
private function remove_element( $element ) {
$element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomDocument
}
/**
* is_a_attribute
* @param $name
* @param $check
*
* @return bool
*/
private function is_a_attribute( $name, $check ) {
return 0 === strpos( $name, $check . '-' );
}
/**
* is_remote_value
* @param $value
*
* @return string
*/
private function is_remote_value( $value ) {
$value = trim( preg_replace( '/[^ -~]/xu', '', $value ) );
$wrapped_in_url = preg_match( '~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match );
if ( ! $wrapped_in_url ) {
return false;
}
$value = trim( $match[1], '\'"' );
return preg_match( '~^((https?|ftp|file):)?//~xi', $value );
}
/**
* has_js_value
* @param $value
*
* @return false|int
*/
private function has_js_value( $value ) {
return preg_match( '/base64|data|(?:java)?script|alert\(|window\.|document/i', $value );
}
/**
* get_allowed_attributes
* @return array
*/
private function get_allowed_attributes() {
$allowed_attributes = [
'class',
'clip-path',
'clip-rule',
'fill',
'fill-opacity',
'fill-rule',
'filter',
'id',
'mask',
'opacity',
'stroke',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-linecap',
'stroke-linejoin',
'stroke-miterlimit',
'stroke-opacity',
'stroke-width',
'style',
'systemlanguage',
'transform',
'href',
'xlink:href',
'xlink:title',
'cx',
'cy',
'r',
'requiredfeatures',
'clippathunits',
'type',
'rx',
'ry',
'color-interpolation-filters',
'stddeviation',
'filterres',
'filterunits',
'height',
'primitiveunits',
'width',
'x',
'y',
'font-size',
'display',
'font-family',
'font-style',
'font-weight',
'text-anchor',
'marker-end',
'marker-mid',
'marker-start',
'x1',
'x2',
'y1',
'y2',
'gradienttransform',
'gradientunits',
'spreadmethod',
'markerheight',
'markerunits',
'markerwidth',
'orient',
'preserveaspectratio',
'refx',
'refy',
'viewbox',
'maskcontentunits',
'maskunits',
'd',
'patterncontentunits',
'patterntransform',
'patternunits',
'points',
'fx',
'fy',
'offset',
'stop-color',
'stop-opacity',
'xmlns',
'xmlns:se',
'xmlns:xlink',
'xml:space',
'method',
'spacing',
'startoffset',
'dx',
'dy',
'rotate',
'textlength',
];
return apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
}
/**
* get_allowed_elements
* @return array
*/
private function get_allowed_elements() {
$allowed_elements = [
'a',
'circle',
'clippath',
'defs',
'style',
'desc',
'ellipse',
'fegaussianblur',
'filter',
'foreignobject',
'g',
'image',
'line',
'lineargradient',
'marker',
'mask',
'metadata',
'path',
'pattern',
'polygon',
'polyline',
'radialgradient',
'rect',
'stop',
'svg',
'switch',
'symbol',
'text',
'textpath',
'title',
'tspan',
'use',
];
return apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
}
/**
* validate_allowed_attributes
* @param \DOMElement $element
*/
private function validate_allowed_attributes( $element ) {
static $allowed_attributes = false;
if ( false === $allowed_attributes ) {
$allowed_attributes = $this->get_allowed_attributes();
}
for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
// get attribute name
$attr_name = $element->attributes->item( $index )->name;
$attr_name_lowercase = strtolower( $attr_name );
// Remove attribute if not in whitelist
if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
$element->removeAttribute( $attr_name );
continue;
}
$attr_value = $element->attributes->item( $index )->value;
// Remove attribute if it has a remote reference or js or data-URI/base64
if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
$element->removeAttribute( $attr_name );
continue;
}
}
}
/**
* strip_xlinks
* @param \DOMElement $element
*/
private function strip_xlinks( $element ) {
$xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
if ( ! $xlinks ) {
return;
}
$allowed_links = [
'data:image/png', // PNG
'data:image/gif', // GIF
'data:image/jpg', // JPG
'data:image/jpe', // JPEG
'data:image/pjp', // PJPEG
];
if ( 1 === preg_match( self::SCRIPT_REGEX, $xlinks ) ) {
if ( ! in_array( substr( $xlinks, 0, 14 ), $allowed_links ) ) {
$element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
}
}
}
/**
* validate_use_tag
* @param $element
*/
private function validate_use_tag( $element ) {
$xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
$element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
}
}
/**
* strip_docktype
*/
private function strip_doctype() {
foreach ( $this->svg_dom->childNodes as $child ) {
if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { // phpcs:ignore -- php DomDocument
$child->parentNode->removeChild( $child ); // phpcs:ignore -- php DomDocument
}
}
}
/**
* sanitize_elements
*/
private function sanitize_elements() {
$elements = $this->svg_dom->getElementsByTagName( '*' );
// loop through all elements
// we do this backwards so we don't skip anything if we delete a node
// see comments at: http://php.net/manual/en/class.domnamednodemap.php
for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
/**
* @var \DOMElement $current_element
*/
$current_element = $elements->item( $index );
// If the tag isn't in the whitelist, remove it and continue with next iteration
if ( ! $this->is_allowed_tag( $current_element ) ) {
continue;
}
//validate element attributes
$this->validate_allowed_attributes( $current_element );
$this->strip_xlinks( $current_element );
if ( 'use' === strtolower( $current_element->tagName ) ) { // phpcs:ignore -- php DomDocument
$this->validate_use_tag( $current_element );
}
}
}
/**
* sanitizer
* @param $content
*
* @return bool|string
*/
public function sanitizer( $content ) {
// Strip php tags
$content = $this->strip_comments( $content );
$content = $this->strip_php_tags( $content );
// Find the start and end tags so we can cut out miscellaneous garbage.
$start = strpos( $content, '<svg' );
$end = strrpos( $content, '</svg>' );
if ( false === $start || false === $end ) {
return false;
}
$content = substr( $content, $start, ( $end - $start + 6 ) );
// Make sure to Disable the ability to load external entities
$libxml_disable_entity_loader = libxml_disable_entity_loader( true );
// Suppress the errors
$libxml_use_internal_errors = libxml_use_internal_errors( true );
// Create DomDocument instance
$this->svg_dom = new \DOMDocument();
$this->svg_dom->formatOutput = false;
$this->svg_dom->preserveWhiteSpace = false;
$this->svg_dom->strictErrorChecking = false;
$open_svg = $this->svg_dom->loadXML( $content );
if ( ! $open_svg ) {
return false;
}
$this->strip_doctype();
$this->sanitize_elements();
// Export sanitized svg to string
// Using documentElement to strip out <?xml version="1.0" encoding="UTF-8"...
$sanitized = $this->svg_dom->saveXML( $this->svg_dom->documentElement, LIBXML_NOEMPTYTAG );
// Restore defaults
libxml_disable_entity_loader( $libxml_disable_entity_loader );
libxml_use_internal_errors( $libxml_use_internal_errors );
return $sanitized;
}
/**
* strip_php_tags
* @param $string
*
* @return string
*/
private function strip_php_tags( $string ) {
$string = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $string );
// Remove XML, ASP, etc.
$string = preg_replace( '/<\?(.*)\?>/Us', '', $string );
$string = preg_replace( '/<\%(.*)\%>/Us', '', $string );
if ( ( false !== strpos( $string, '<?' ) ) || ( false !== strpos( $string, '<%' ) ) ) {
return '';
}
return $string;
}
/**
* strip_comments
* @param $string
*
* @return string
*/
private function strip_comments( $string ) {
// Remove comments.
$string = preg_replace( '/<!--(.*)-->/Us', '', $string );
$string = preg_replace( '/\/\*(.*)\*\//Us', '', $string );
if ( ( false !== strpos( $string, '<!--' ) ) || ( false !== strpos( $string, '/*' ) ) ) {
return '';
}
return $string;
}
/**
* wp_prepare_attachment_for_js
* @param $attachment_data
* @param $attachment
* @param $meta
*
* @return mixed
*/
public function wp_prepare_attachment_for_js( $attachment_data, $attachment, $meta ) {
if ( 'image' !== $attachment_data['type'] || 'svg+xml' !== $attachment_data['subtype'] || ! class_exists( 'SimpleXMLElement' ) ) {
return $attachment_data;
}
$svg = self::get_inline_svg( $attachment->ID );
if ( ! $svg ) {
return $attachment_data;
}
try {
$svg = new \SimpleXMLElement( $svg );
} catch ( \Exception $e ) {
return $attachment_data;
}
$src = $attachment_data['url'];
$width = (int) $svg['width'];
$height = (int) $svg['height'];
// Media Gallery
$attachment_data['image'] = compact( 'src', 'width', 'height' );
$attachment_data['thumb'] = compact( 'src', 'width', 'height' );
// Single Details of Image
$attachment_data['sizes']['full'] = [
'height' => $height,
'width' => $width,
'url' => $src,
'orientation' => $height > $width ? 'portrait' : 'landscape',
];
return $attachment_data;
}
/**
* set_attachment_id
* @param $attachment_id
*
* @return int
*/
public function set_attachment_id( $attachment_id ) {
$this->attachment_id = $attachment_id;
return $this->attachment_id;
}
/**
* get_attachment_id
* @return int
*/
public function get_attachment_id() {
return $this->attachment_id;
}
/**
* handle_upload_prefilter
* @param $file
*
* @return mixed
*/
public function handle_upload_prefilter( $file ) {
if ( ! $this->is_file_should_handled( $file ) ) {
return $file;
}
$file = parent::handle_upload_prefilter( $file );
if ( ! $file['error'] && self::file_sanitizer_can_run() && ! $this->sanitize_svg( $file['tmp_name'] ) ) {
$display_type = strtoupper( $this->get_file_type() );
$file['error'] = sprintf( __( 'Invalid %1$s Format, file not uploaded for security reasons', 'elementor' ), $display_type );
}
return $file;
}
/**
* @since 3.0.0
* @deprecated 3.0.0 Use Files_Upload_Handler::file_sanitizer_can_run() instead.
*/
public function svg_sanitizer_can_run() {
_deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::file_sanitizer_can_run()' );
return Files_Upload_Handler::file_sanitizer_can_run();
}
/**
* @since 3.0.0
* @deprecated 3.0.0
*/
public function upload_mimes() {
_deprecated_function( __METHOD__, '3.0.0' );
}
/**
* @since 3.0.0
* @deprecated 3.0.0
*/
public function wp_handle_upload_prefilter() {
_deprecated_function( __METHOD__, '3.0.0' );
}
/**
* @since 3.0.0
* @deprecated 3.0.0 Use Files_Upload_Handler::is_enabled() instead.
* @see is_enabled()
*/
public function is_svg_uploads_enabled() {
_deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::is_enabled()' );
return Files_Upload_Handler::is_enabled();
}
/**
* Svg_Handler constructor.
*/
public function __construct() {
parent::__construct();
add_filter( 'wp_prepare_attachment_for_js', [ $this, 'wp_prepare_attachment_for_js' ], 10, 3 );
add_action( 'elementor/core/files/clear_cache', [ $this, 'delete_meta_cache' ] );
}
}
@@ -0,0 +1,304 @@
<?php
namespace Elementor\Core\Files;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Base {
const UPLOADS_DIR = 'elementor/';
const DEFAULT_FILES_DIR = 'css/';
const META_KEY = '';
private static $wp_uploads_dir = [];
private $files_dir;
private $file_name;
/**
* File path.
*
* Holds the file path.
*
* @access private
*
* @var string
*/
private $path;
/**
* Content.
*
* Holds the file content.
*
* @access private
*
* @var string
*/
private $content;
/**
* @since 2.1.0
* @access public
* @static
*/
public static function get_base_uploads_dir() {
$wp_upload_dir = self::get_wp_uploads_dir();
return $wp_upload_dir['basedir'] . '/' . self::UPLOADS_DIR;
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function get_base_uploads_url() {
$wp_upload_dir = self::get_wp_uploads_dir();
return $wp_upload_dir['baseurl'] . '/' . self::UPLOADS_DIR;
}
/**
* Use a create function for PhpDoc (@return static).
*
* @return static
*/
public static function create() {
return Plugin::$instance->files_manager->get( get_called_class(), func_get_args() );
}
/**
* @since 2.1.0
* @access public
*/
public function __construct( $file_name ) {
/**
* Elementor File Name
*
* Filters the File name
*
* @since 2.3.0
*
* @param string $file_name
* @param object $this The file instance, which inherits Elementor\Core\Files
*/
$file_name = apply_filters( 'elementor/files/file_name', $file_name, $this );
$this->set_file_name( $file_name );
$this->set_files_dir( static::DEFAULT_FILES_DIR );
$this->set_path();
}
/**
* @since 2.1.0
* @access public
*/
public function set_files_dir( $files_dir ) {
$this->files_dir = $files_dir;
}
/**
* @since 2.1.0
* @access public
*/
public function set_file_name( $file_name ) {
$this->file_name = $file_name;
}
/**
* @since 2.1.0
* @access public
*/
public function get_file_name() {
return $this->file_name;
}
/**
* @since 2.1.0
* @access public
*/
public function get_url() {
$url = set_url_scheme( self::get_base_uploads_url() . $this->files_dir . $this->file_name );
return add_query_arg( [ 'ver' => $this->get_meta( 'time' ) ], $url );
}
/**
* @since 2.1.0
* @access public
*/
public function get_content() {
if ( ! $this->content ) {
$this->content = $this->parse_content();
}
return $this->content;
}
/**
* @since 2.1.0
* @access public
*/
public function update() {
$this->update_file();
$meta = $this->get_meta();
$meta['time'] = time();
$this->update_meta( $meta );
}
/**
* @since 2.1.0
* @access public
*/
public function update_file() {
$this->content = $this->parse_content();
if ( $this->content ) {
$this->write();
} else {
$this->delete();
}
}
/**
* @since 2.1.0
* @access public
*/
public function write() {
return file_put_contents( $this->path, $this->content );
}
/**
* @since 2.1.0
* @access public
*/
public function delete() {
if ( file_exists( $this->path ) ) {
unlink( $this->path );
}
$this->delete_meta();
}
/**
* Get meta data.
*
* Retrieve the CSS file meta data. Returns an array of all the data, or if
* custom property is given it will return the property value, or `null` if
* the property does not exist.
*
* @since 2.1.0
* @access public
*
* @param string $property Optional. Custom meta data property. Default is
* null.
*
* @return array|null An array of all the data, or if custom property is
* given it will return the property value, or `null` if
* the property does not exist.
*/
public function get_meta( $property = null ) {
$meta = array_merge( $this->get_default_meta(), (array) $this->load_meta() );
if ( $property ) {
return isset( $meta[ $property ] ) ? $meta[ $property ] : null;
}
return $meta;
}
/**
* @since 2.1.0
* @access protected
* @abstract
*/
abstract protected function parse_content();
/**
* Load meta.
*
* Retrieve the file meta data.
*
* @since 2.1.0
* @access protected
*/
protected function load_meta() {
return get_option( static::META_KEY );
}
/**
* Update meta.
*
* Update the file meta data.
*
* @since 2.1.0
* @access protected
*
* @param array $meta New meta data.
*/
protected function update_meta( $meta ) {
update_option( static::META_KEY, $meta );
}
/**
* Delete meta.
*
* Delete the file meta data.
*
* @since 2.1.0
* @access protected
*/
protected function delete_meta() {
delete_option( static::META_KEY );
}
/**
* @since 2.1.0
* @access protected
*/
protected function get_default_meta() {
return [
'time' => 0,
];
}
/**
* @since 2.1.0
* @access private
* @static
*/
private static function get_wp_uploads_dir() {
global $blog_id;
if ( empty( self::$wp_uploads_dir[ $blog_id ] ) ) {
self::$wp_uploads_dir[ $blog_id ] = wp_upload_dir( null, false );
}
return self::$wp_uploads_dir[ $blog_id ];
}
/**
* @since 2.1.0
* @access private
*/
private function set_path() {
$dir_path = self::get_base_uploads_dir() . $this->files_dir;
if ( ! is_dir( $dir_path ) ) {
wp_mkdir_p( $dir_path );
}
$this->path = $dir_path . $this->file_name;
}
}
@@ -0,0 +1,902 @@
<?php
namespace Elementor\Core\Files\CSS;
use Elementor\Base_Data_Control;
use Elementor\Control_Repeater;
use Elementor\Controls_Manager;
use Elementor\Controls_Stack;
use Elementor\Core\Files\Base as Base_File;
use Elementor\Core\DynamicTags\Manager;
use Elementor\Core\DynamicTags\Tag;
use Elementor\Core\Kits\Documents\Tabs\Global_Typography;
use Elementor\Element_Base;
use Elementor\Plugin;
use Elementor\Core\Responsive\Responsive;
use Elementor\Stylesheet;
use Elementor\Icons_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor CSS file.
*
* Elementor CSS file handler class is responsible for generating CSS files.
*
* @since 1.2.0
* @abstract
*/
abstract class Base extends Base_File {
/**
* Elementor CSS file generated status.
*
* The parsing result after generating CSS file.
*/
const CSS_STATUS_FILE = 'file';
/**
* Elementor inline CSS status.
*
* The parsing result after generating inline CSS.
*/
const CSS_STATUS_INLINE = 'inline';
/**
* Elementor CSS empty status.
*
* The parsing result when an empty CSS returned.
*/
const CSS_STATUS_EMPTY = 'empty';
/**
* Fonts.
*
* Holds the list of fonts.
*
* @access private
*
* @var array
*/
private $fonts = [];
private $icons_fonts = [];
private $dynamic_elements_ids = [];
/**
* Stylesheet object.
*
* Holds the CSS file stylesheet instance.
*
* @access protected
*
* @var Stylesheet
*/
protected $stylesheet_obj;
/**
* Printed.
*
* Holds the list of printed files.
*
* @access protected
*
* @var array
*/
private static $printed = [];
/**
* Get CSS file name.
*
* Retrieve the CSS file name.
*
* @since 1.6.0
* @access public
* @abstract
*/
abstract public function get_name();
protected function is_global_parsing_supported() {
return false;
}
/**
* Use external file.
*
* Whether to use external CSS file of not. When there are new schemes or settings
* updates.
*
* @since 1.9.0
* @access protected
*
* @return bool True if the CSS requires an update, False otherwise.
*/
protected function use_external_file() {
return 'internal' !== get_option( 'elementor_css_print_method' );
}
/**
* Update the CSS file.
*
* Delete old CSS, parse the CSS, save the new file and update the database.
*
* This method also sets the CSS status to be used later on in the render posses.
*
* @since 1.2.0
* @access public
*/
public function update() {
$this->update_file();
$meta = $this->get_meta();
$meta['time'] = time();
$content = $this->get_content();
if ( empty( $content ) ) {
$meta['status'] = self::CSS_STATUS_EMPTY;
$meta['css'] = '';
} else {
$use_external_file = $this->use_external_file();
if ( $use_external_file ) {
$meta['status'] = self::CSS_STATUS_FILE;
} else {
$meta['status'] = self::CSS_STATUS_INLINE;
$meta['css'] = $content;
}
}
$meta['dynamic_elements_ids'] = $this->dynamic_elements_ids;
$this->update_meta( $meta );
}
/**
* @since 2.1.0
* @access public
*/
public function write() {
if ( $this->use_external_file() ) {
parent::write();
}
}
/**
* @since 3.0.0
* @access public
*/
public function delete() {
if ( $this->use_external_file() ) {
parent::delete();
} else {
$this->delete_meta();
}
}
/**
* Enqueue CSS.
*
* Either enqueue the CSS file in Elementor or add inline style.
*
* This method is also responsible for loading the fonts.
*
* @since 1.2.0
* @access public
*/
public function enqueue() {
$handle_id = $this->get_file_handle_id();
if ( isset( self::$printed[ $handle_id ] ) ) {
return;
}
self::$printed[ $handle_id ] = true;
$meta = $this->get_meta();
if ( self::CSS_STATUS_EMPTY === $meta['status'] ) {
return;
}
// First time after clear cache and etc.
if ( '' === $meta['status'] || $this->is_update_required() ) {
$this->update();
$meta = $this->get_meta();
}
if ( self::CSS_STATUS_INLINE === $meta['status'] ) {
$dep = $this->get_inline_dependency();
// If the dependency has already been printed ( like a template in footer )
if ( wp_styles()->query( $dep, 'done' ) ) {
printf( '<style id="%1$s">%2$s</style>', $this->get_file_handle_id(), $meta['css'] ); // XSS ok.
} else {
wp_add_inline_style( $dep, $meta['css'] );
}
} elseif ( self::CSS_STATUS_FILE === $meta['status'] ) { // Re-check if it's not empty after CSS update.
wp_enqueue_style( $this->get_file_handle_id(), $this->get_url(), $this->get_enqueue_dependencies(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
}
// Handle fonts.
if ( ! empty( $meta['fonts'] ) ) {
foreach ( $meta['fonts'] as $font ) {
Plugin::$instance->frontend->enqueue_font( $font );
}
}
if ( ! empty( $meta['icons'] ) ) {
$icons_types = Icons_Manager::get_icon_manager_tabs();
foreach ( $meta['icons'] as $icon_font ) {
if ( ! isset( $icons_types[ $icon_font ] ) ) {
continue;
}
Plugin::$instance->frontend->enqueue_font( $icon_font );
}
}
$name = $this->get_name();
/**
* Enqueue CSS file.
*
* Fires when CSS file is enqueued on Elementor.
*
* The dynamic portion of the hook name, `$name`, refers to the CSS file name.
*
* @since 2.0.0
*
* @param Base $this The current CSS file.
*/
do_action( "elementor/css-file/{$name}/enqueue", $this );
}
/**
* Print CSS.
*
* Output the final CSS inside the `<style>` tags and all the frontend fonts in
* use.
*
* @since 1.9.4
* @access public
*/
public function print_css() {
echo '<style>' . $this->get_content() . '</style>'; // XSS ok.
Plugin::$instance->frontend->print_fonts_links();
}
/**
* Add control rules.
*
* Parse the CSS for all the elements inside any given control.
*
* This method recursively renders the CSS for all the selectors in the control.
*
* @since 1.2.0
* @access public
*
* @param array $control The controls.
* @param array $controls_stack The controls stack.
* @param callable $value_callback Callback function for the value.
* @param array $placeholders Placeholders.
* @param array $replacements Replacements.
* @param array $values Global Values.
*/
public function add_control_rules( array $control, array $controls_stack, callable $value_callback, array $placeholders, array $replacements, array $values = [] ) {
if ( empty( $control['selectors'] ) ) {
return;
}
$control_global_key = $control['name'];
if ( ! empty( $control['groupType'] ) ) {
$control_global_key = $control['groupPrefix'] . $control['groupType'];
}
$global_values = [];
$global_key = '';
if ( ! empty( $values['__globals__'] ) ) {
$global_values = $values['__globals__'];
}
if ( ! empty( $global_values[ $control_global_key ] ) ) {
$global_key = $global_values[ $control_global_key ];
}
if ( ! $global_key ) {
$value = call_user_func( $value_callback, $control );
if ( null === $value ) {
return;
}
}
$stylesheet = $this->get_stylesheet();
foreach ( $control['selectors'] as $selector => $css_property ) {
$output_css_property = '';
if ( $global_key ) {
$selector_global_value = $this->get_selector_global_value( $control, $global_key );
if ( $selector_global_value ) {
$output_css_property = preg_replace( '/(:)[^;]+(;?)/', '$1' . $selector_global_value . '$2', $css_property );
}
} else {
try {
$output_css_property = preg_replace_callback( '/{{(?:([^.}]+)\.)?([^}| ]*)(?: *\|\| *(?:([^.}]+)\.)?([^}| ]*) *)*}}/', function( $matches ) use ( $control, $value_callback, $controls_stack, $value, $css_property ) {
$external_control_missing = $matches[1] && ! isset( $controls_stack[ $matches[1] ] );
$parsed_value = '';
if ( ! $external_control_missing ) {
$parsed_value = $this->parse_property_placeholder( $control, $value, $controls_stack, $value_callback, $matches[2], $matches[1] );
}
if ( '' === $parsed_value ) {
if ( isset( $matches[4] ) ) {
$parsed_value = $matches[4];
$is_string_value = preg_match( '/^([\'"])(.*)\1$/', $parsed_value, $string_matches );
if ( $is_string_value ) {
$parsed_value = $string_matches[2];
} elseif ( ! is_numeric( $parsed_value ) ) {
if ( $matches[3] && ! isset( $controls_stack[ $matches[3] ] ) ) {
return '';
}
$parsed_value = $this->parse_property_placeholder( $control, $value, $controls_stack, $value_callback, $matches[4], $matches[3] );
}
}
if ( '' === $parsed_value ) {
if ( $external_control_missing ) {
return '';
}
throw new \Exception();
}
}
return $parsed_value;
}, $css_property );
} catch ( \Exception $e ) {
return;
}
}
if ( ! $output_css_property ) {
continue;
}
$device_pattern = '/^(?:\([^\)]+\)){1,2}/';
preg_match( $device_pattern, $selector, $device_rules );
$query = [];
if ( $device_rules ) {
$selector = preg_replace( $device_pattern, '', $selector );
preg_match_all( '/\(([^)]+)\)/', $device_rules[0], $pure_device_rules );
$pure_device_rules = $pure_device_rules[1];
foreach ( $pure_device_rules as $device_rule ) {
if ( Element_Base::RESPONSIVE_DESKTOP === $device_rule ) {
continue;
}
$device = preg_replace( '/\+$/', '', $device_rule );
$endpoint = $device === $device_rule ? 'max' : 'min';
$query[ $endpoint ] = $device;
}
}
$parsed_selector = str_replace( $placeholders, $replacements, $selector );
if ( ! $query && ! empty( $control['responsive'] ) ) {
$query = array_intersect_key( $control['responsive'], array_flip( [ 'min', 'max' ] ) );
if ( ! empty( $query['max'] ) && Element_Base::RESPONSIVE_DESKTOP === $query['max'] ) {
unset( $query['max'] );
}
}
$stylesheet->add_rules( $parsed_selector, $output_css_property, $query );
}
}
/**
* @param array $control
* @param mixed $value
* @param array $controls_stack
* @param callable $value_callback
* @param string $placeholder
* @param string $parser_control_name
*
* @return string
*/
public function parse_property_placeholder( array $control, $value, array $controls_stack, $value_callback, $placeholder, $parser_control_name = null ) {
if ( $parser_control_name ) {
$control = $controls_stack[ $parser_control_name ];
$value = call_user_func( $value_callback, $control );
}
if ( Controls_Manager::FONT === $control['type'] ) {
$this->fonts[] = $value;
}
/** @var Base_Data_Control $control_obj */
$control_obj = Plugin::$instance->controls_manager->get_control( $control['type'] );
return (string) $control_obj->get_style_value( $placeholder, $value, $control );
}
/**
* Get the fonts.
*
* Retrieve the list of fonts.
*
* @since 1.9.0
* @access public
*
* @return array Fonts.
*/
public function get_fonts() {
return $this->fonts;
}
/**
* Get stylesheet.
*
* Retrieve the CSS file stylesheet instance.
*
* @since 1.2.0
* @access public
*
* @return Stylesheet The stylesheet object.
*/
public function get_stylesheet() {
if ( ! $this->stylesheet_obj ) {
$this->init_stylesheet();
}
return $this->stylesheet_obj;
}
/**
* Add controls stack style rules.
*
* Parse the CSS for all the elements inside any given controls stack.
*
* This method recursively renders the CSS for all the child elements in the stack.
*
* @since 1.6.0
* @access public
*
* @param Controls_Stack $controls_stack The controls stack.
* @param array $controls Controls array.
* @param array $values Values array.
* @param array $placeholders Placeholders.
* @param array $replacements Replacements.
* @param array $all_controls All controls.
*/
public function add_controls_stack_style_rules( Controls_Stack $controls_stack, array $controls, array $values, array $placeholders, array $replacements, array $all_controls = null ) {
if ( ! $all_controls ) {
$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[ Manager::DYNAMIC_SETTING_KEY ][ $control['name'] ] ) ) {
$this->add_dynamic_control_style_rules( $control, $control[ Manager::DYNAMIC_SETTING_KEY ][ $control['name'] ] );
}
if ( Controls_Manager::ICONS === $control['type'] ) {
$this->icons_fonts[] = $values[ $control['name'] ]['library'];
}
if ( ! empty( $parsed_dynamic_settings[ Manager::DYNAMIC_SETTING_KEY ][ $control['name'] ] ) ) {
// Dynamic CSS should not be added to the CSS files.
// Instead it's handled by \Elementor\Core\DynamicTags\Dynamic_CSS
// and printed in a style tag.
unset( $parsed_dynamic_settings[ $control['name'] ] );
$this->dynamic_elements_ids[] = $controls_stack->get_id();
continue;
}
if ( empty( $control['selectors'] ) ) {
continue;
}
$this->add_control_style_rules( $control, $parsed_dynamic_settings, $all_controls, $placeholders, $replacements );
}
}
/**
* Get file handle ID.
*
* Retrieve the file handle ID.
*
* @since 1.2.0
* @access protected
* @abstract
*
* @return string CSS file handle ID.
*/
abstract protected function get_file_handle_id();
/**
* Render CSS.
*
* Parse the CSS.
*
* @since 1.2.0
* @access protected
* @abstract
*/
abstract protected function render_css();
protected function get_default_meta() {
return array_merge( parent::get_default_meta(), [
'fonts' => array_unique( $this->fonts ),
'icons' => array_unique( $this->icons_fonts ),
'dynamic_elements_ids' => [],
'status' => '',
] );
}
/**
* Get enqueue dependencies.
*
* Retrieve the name of the stylesheet used by `wp_enqueue_style()`.
*
* @since 1.2.0
* @access protected
*
* @return array Name of the stylesheet.
*/
protected function get_enqueue_dependencies() {
return [];
}
/**
* Get inline dependency.
*
* Retrieve the name of the stylesheet used by `wp_add_inline_style()`.
*
* @since 1.2.0
* @access protected
*
* @return string Name of the stylesheet.
*/
protected function get_inline_dependency() {
return '';
}
/**
* Is update required.
*
* Whether the CSS requires an update. When there are new schemes or settings
* updates.
*
* @since 1.2.0
* @access protected
*
* @return bool True if the CSS requires an update, False otherwise.
*/
protected function is_update_required() {
return false;
}
/**
* Parse CSS.
*
* Parsing the CSS file.
*
* @since 1.2.0
* @access protected
*/
protected function parse_content() {
$this->render_css();
$name = $this->get_name();
/**
* Parse CSS file.
*
* Fires when CSS file is parsed on Elementor.
*
* The dynamic portion of the hook name, `$name`, refers to the CSS file name.
*
* @since 2.0.0
*
* @param Base $this The current CSS file.
*/
do_action( "elementor/css-file/{$name}/parse", $this );
return $this->get_stylesheet()->__toString();
}
/**
* Add control style rules.
*
* Register new style rules for the control.
*
* @since 1.6.0
* @access private
*
* @param array $control The control.
* @param array $values Values array.
* @param array $controls The controls stack.
* @param array $placeholders Placeholders.
* @param array $replacements Replacements.
*/
protected function add_control_style_rules( array $control, array $values, array $controls, array $placeholders, array $replacements ) {
$this->add_control_rules(
$control, $controls, function( $control ) use ( $values ) {
return $this->get_style_control_value( $control, $values );
}, $placeholders, $replacements, $values
);
}
/**
* Get style control value.
*
* Retrieve the value of the style control for any give control and values.
*
* It will retrieve the control name and return the style value.
*
* @since 1.6.0
* @access private
*
* @param array $control The control.
* @param array $values Values array.
*
* @return mixed Style control value.
*/
private function get_style_control_value( array $control, array $values ) {
if ( ! empty( $values['__globals__'][ $control['name'] ] ) ) {
// When the control itself has no global value, but it refers to another control global value
return $this->get_selector_global_value( $control, $values['__globals__'][ $control['name'] ] );
}
$value = $values[ $control['name'] ];
if ( isset( $control['selectors_dictionary'][ $value ] ) ) {
$value = $control['selectors_dictionary'][ $value ];
}
if ( ! is_numeric( $value ) && ! is_float( $value ) && empty( $value ) ) {
return null;
}
return $value;
}
/**
* Init stylesheet.
*
* Initialize CSS file stylesheet by creating a new `Stylesheet` object and register new
* breakpoints for the stylesheet.
*
* @since 1.2.0
* @access private
*/
private function init_stylesheet() {
$this->stylesheet_obj = new Stylesheet();
$breakpoints = Responsive::get_breakpoints();
$this->stylesheet_obj
->add_device( 'mobile', 0 )
->add_device( 'tablet', $breakpoints['md'] )
->add_device( 'desktop', $breakpoints['lg'] );
}
/**
* Add repeater control style rules.
*
* Register new style rules for the repeater control.
*
* @since 2.0.0
* @access private
*
* @param Controls_Stack $controls_stack The control stack.
* @param array $repeater_control The repeater control.
* @param array $repeater_values Repeater values array.
* @param array $placeholders Placeholders.
* @param array $replacements Replacements.
*/
protected function add_repeater_control_style_rules( Controls_Stack $controls_stack, array $repeater_control, array $repeater_values, array $placeholders, array $replacements ) {
$placeholders = array_merge( $placeholders, [ '{{CURRENT_ITEM}}' ] );
foreach ( $repeater_control['style_fields'] as $index => $item ) {
$this->add_controls_stack_style_rules(
$controls_stack,
$item,
$repeater_values[ $index ],
$placeholders,
array_merge( $replacements, [ '.elementor-repeater-item-' . $repeater_values[ $index ]['_id'] ] ),
$repeater_control['fields']
);
}
}
/**
* Add dynamic control style rules.
*
* Register new style rules for the dynamic control.
*
* @since 2.0.0
* @access private
*
* @param array $control The control.
* @param string $value The value.
*/
protected function add_dynamic_control_style_rules( array $control, $value ) {
Plugin::$instance->dynamic_tags->parse_tags_text( $value, $control, function( $id, $name, $settings ) {
$tag = Plugin::$instance->dynamic_tags->create_tag( $id, $name, $settings );
if ( ! $tag instanceof Tag ) {
return;
}
$this->add_controls_stack_style_rules( $tag, $this->get_style_controls( $tag ), $tag->get_active_settings(), [ '{{WRAPPER}}' ], [ '#elementor-tag-' . $id ] );
} );
}
private function get_selector_global_value( $control, $global_key ) {
$data = Plugin::$instance->data_manager->run( $global_key );
if ( empty( $data['value'] ) ) {
return null;
}
$global_args = explode( '?id=', $global_key );
$id = $global_args[1];
if ( ! empty( $control['groupType'] ) ) {
$property_name = str_replace( [ $control['groupPrefix'], '_tablet', '_mobile' ], '', $control['name'] );
// TODO: This check won't retrieve the proper answer for array values (multiple controls).
if ( empty( $data['value'][ Global_Typography::TYPOGRAPHY_GROUP_PREFIX . $property_name ] ) ) {
return null;
}
$property_name = str_replace( '_', '-', $property_name );
$value = "var( --e-global-$control[groupType]-$id-$property_name )";
} else {
$value = "var( --e-global-$control[type]-$id )";
}
return $value;
}
final protected function get_active_controls( Controls_Stack $controls_stack, array $controls = null, array $settings = null ) {
if ( ! $controls ) {
$controls = $controls_stack->get_controls();
}
if ( ! $settings ) {
$settings = $controls_stack->get_controls_settings();
}
if ( $this->is_global_parsing_supported() ) {
$settings = $this->parse_global_settings( $settings, $controls );
}
$active_controls = array_reduce(
array_keys( $controls ), function( $active_controls, $control_key ) use ( $controls_stack, $controls, $settings ) {
$control = $controls[ $control_key ];
if ( $controls_stack->is_control_visible( $control, $settings ) ) {
$active_controls[ $control_key ] = $control;
}
return $active_controls;
}, []
);
return $active_controls;
}
final public function get_style_controls( Controls_Stack $controls_stack, array $controls = null, array $settings = null ) {
$controls = $this->get_active_controls( $controls_stack, $controls, $settings );
$style_controls = [];
foreach ( $controls as $control_name => $control ) {
$control_obj = Plugin::$instance->controls_manager->get_control( $control['type'] );
if ( ! $control_obj instanceof Base_Data_Control ) {
continue;
}
$control = array_merge( $control_obj->get_settings(), $control );
if ( $control_obj instanceof Control_Repeater ) {
$style_fields = [];
foreach ( $controls_stack->get_settings( $control_name ) as $item ) {
$style_fields[] = $this->get_style_controls( $controls_stack, $control['fields'], $item );
}
$control['style_fields'] = $style_fields;
}
if ( ! empty( $control['selectors'] ) || ! empty( $control['dynamic'] ) || $this->is_global_control( $controls_stack, $control_name, $controls ) || ! empty( $control['style_fields'] ) ) {
$style_controls[ $control_name ] = $control;
}
}
return $style_controls;
}
private function parse_global_settings( array $settings, array $controls ) {
foreach ( $controls as $control ) {
$control_name = $control['name'];
$control_obj = Plugin::$instance->controls_manager->get_control( $control['type'] );
if ( ! $control_obj instanceof Base_Data_Control ) {
continue;
}
if ( $control_obj instanceof Control_Repeater ) {
foreach ( $settings[ $control_name ] as & $field ) {
$field = $this->parse_global_settings( $field, $control['fields'] );
}
continue;
}
if ( empty( $control['global']['active'] ) ) {
continue;
}
if ( empty( $settings['__globals__'][ $control_name ] ) ) {
continue;
}
$settings[ $control_name ] = 'global';
}
return $settings;
}
private function is_global_control( Controls_Stack $controls_stack, $control_name, $controls ) {
$control = $controls[ $control_name ];
$control_global_key = $control_name;
if ( ! empty( $control['groupType'] ) ) {
$control_global_key = $control['groupPrefix'] . $control['groupType'];
}
if ( empty( $controls[ $control_global_key ]['global']['active'] ) ) {
return false;
}
$globals = $controls_stack->get_settings( '__globals__' );
return ! empty( $globals[ $control_global_key ] );
}
}
@@ -0,0 +1,159 @@
<?php
namespace Elementor\Core\Files\CSS;
use Elementor\Core\Kits\Manager;
use Elementor\Plugin;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor global CSS file.
*
* Elementor CSS file handler class is responsible for generating the global CSS
* file.
*
* @since 1.2.0
*/
class Global_CSS extends Base {
/**
* Elementor global CSS file handler ID.
*/
const FILE_HANDLER_ID = 'elementor-global';
const META_KEY = '_elementor_global_css';
/**
* Get CSS file name.
*
* Retrieve the CSS file name.
*
* @since 1.6.0
* @access public
*
* @return string CSS file name.
*/
public function get_name() {
return 'global';
}
/**
* Get file handle ID.
*
* Retrieve the handle ID for the global post CSS file.
*
* @since 1.2.0
* @access protected
*
* @return string CSS file handle ID.
*/
protected function get_file_handle_id() {
return self::FILE_HANDLER_ID;
}
/**
* Render CSS.
*
* Parse the CSS for all the widgets and all the scheme controls.
*
* @since 1.2.0
* @access protected
*/
protected function render_css() {
$this->render_schemes_and_globals_css();
}
/**
* Get inline dependency.
*
* Retrieve the name of the stylesheet used by `wp_add_inline_style()`.
*
* @since 1.2.0
* @access protected
*
* @return string Name of the stylesheet.
*/
protected function get_inline_dependency() {
return 'elementor-frontend';
}
/**
* Is update required.
*
* Whether the CSS requires an update. When there are new schemes or settings
* updates.
*
* @since 1.2.0
* @access protected
*
* @return bool True if the CSS requires an update, False otherwise.
*/
protected function is_update_required() {
return $this->get_meta( 'time' ) < get_option( Settings::UPDATE_TIME_FIELD );
}
/**
* Render schemes CSS.
*
* Parse the CSS for all the widgets and all the scheme controls.
*
* @since 1.2.0
* @access private
*/
private function render_schemes_and_globals_css() {
$elementor = Plugin::$instance;
/** @var Manager $module */
$kits_manager = Plugin::$instance->kits_manager;
$custom_colors_enabled = $kits_manager->is_custom_colors_enabled();
$custom_typography_enabled = $kits_manager->is_custom_typography_enabled();
// If both default colors and typography are disabled, there is no need to render schemes and default global css.
if ( ! $custom_colors_enabled && ! $custom_typography_enabled ) {
return;
}
foreach ( $elementor->widgets_manager->get_widget_types() as $widget ) {
$controls = $widget->get_controls();
$global_controls = [];
$global_values['__globals__'] = [];
foreach ( $controls as $control ) {
$is_color_control = 'color' === $control['type'];
$is_typography_control = isset( $control['groupType'] ) && 'typography' === $control['groupType'];
// If it is a color/typography control and default colors/typography are disabled,
// don't add the default CSS.
if ( ( $is_color_control && ! $custom_colors_enabled ) || ( $is_typography_control && ! $custom_typography_enabled ) ) {
continue;
}
$global_control = $control;
// Handle group controls that don't have a default global property.
if ( ! empty( $control['groupType'] ) ) {
$global_control = $controls[ $control['groupPrefix'] . $control['groupType'] ];
}
// If the control has a default global defined, add it to the globals array
// that is used in add_control_rules.
if ( ! empty( $control['global']['default'] ) ) {
$global_values['__globals__'][ $control['name'] ] = $global_control['global']['default'];
}
if ( ! empty( $global_control['global']['default'] ) ) {
$global_controls[] = $control;
}
}
foreach ( $global_controls as $control ) {
$this->add_control_rules( $control, $controls, function( $control ) {}, [ '{{WRAPPER}}' ], [ '.elementor-widget-' . $widget->get_name() ], $global_values );
}
}
}
}
@@ -0,0 +1,43 @@
<?php
namespace Elementor\Core\Files\CSS;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Post_Local_Cache extends Post {
/**
* Meta cache
*
* @var array
*/
private $meta_cache = [];
abstract protected function get_post_id_for_data();
public function is_update_required() {
return true;
}
protected function load_meta() {
return $this->meta_cache;
}
protected function delete_meta() {
$this->meta_cache = [];
}
protected function update_meta( $meta ) {
$this->meta_cache = $meta;
}
protected function get_data() {
$document = Plugin::$instance->documents->get( $this->get_post_id_for_data() );
return $document ? $document->get_elements_data() : [];
}
}
@@ -0,0 +1,76 @@
<?php
namespace Elementor\Core\Files\CSS;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor post preview CSS file.
*
* Elementor CSS file handler class is responsible for generating the post
* preview CSS file.
*
* @since 1.9.0
*/
class Post_Preview extends Post_Local_Cache {
/**
* Preview ID.
*
* Holds the ID of the current post being previewed.
*
* @var int
*/
private $post_id_for_data;
/**
* Post preview CSS file constructor.
*
* Initializing the CSS file of the post preview. Set the post ID and the
* parent ID and initiate the stylesheet.
*
* @since 1.9.0
* @access public
*
* @param int $post_id Post ID.
*/
public function __construct( $post_id ) {
$this->post_id_for_data = $post_id;
$parent_id = wp_get_post_parent_id( $post_id );
parent::__construct( $parent_id );
}
protected function get_post_id_for_data() {
return $this->post_id_for_data;
}
/**
* @since 2.1.0
* @access public
* @deprecated 3.0.0 Use `Post_Preview::get_post_id_for_data()` instead
*/
protected function get_preview_id() {
_deprecated_function( __METHOD__, '3.0.0', __CLASS__ . '::get_post_id_for_data()' );
return $this->get_post_id_for_data();
}
/**
* Get file handle ID.
*
* Retrieve the handle ID for the previewed post CSS file.
*
* @since 1.9.0
* @access protected
*
* @return string CSS file handle ID.
*/
protected function get_file_handle_id() {
return 'elementor-preview-' . $this->get_post_id_for_data();
}
}
@@ -0,0 +1,310 @@
<?php
namespace Elementor\Core\Files\CSS;
use Elementor\Base_Data_Control;
use Elementor\Control_Repeater;
use Elementor\Controls_Stack;
use Elementor\Element_Base;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor post CSS file.
*
* Elementor CSS file handler class is responsible for generating the single
* post CSS file.
*
* @since 1.2.0
*/
class Post extends Base {
/**
* Elementor post CSS file prefix.
*/
const FILE_PREFIX = 'post-';
const META_KEY = '_elementor_css';
/**
* Post ID.
*
* Holds the current post ID.
*
* @var int
*/
private $post_id;
protected function is_global_parsing_supported() {
return true;
}
/**
* Post CSS file constructor.
*
* Initializing the CSS file of the post. Set the post ID and initiate the stylesheet.
*
* @since 1.2.0
* @access public
*
* @param int $post_id Post ID.
*/
public function __construct( $post_id ) {
$this->post_id = $post_id;
parent::__construct( static::FILE_PREFIX . $post_id . '.css' );
}
/**
* Get CSS file name.
*
* Retrieve the CSS file name.
*
* @since 1.6.0
* @access public
*
* @return string CSS file name.
*/
public function get_name() {
return 'post';
}
/**
* Get post ID.
*
* Retrieve the ID of current post.
*
* @since 1.2.0
* @access public
*
* @return int Post ID.
*/
public function get_post_id() {
return $this->post_id;
}
/**
* Get unique element selector.
*
* Retrieve the unique selector for any given element.
*
* @since 1.2.0
* @access public
*
* @param Element_Base $element The element.
*
* @return string Unique element selector.
*/
public function get_element_unique_selector( Element_Base $element ) {
return '.elementor-' . $this->post_id . ' .elementor-element' . $element->get_unique_selector();
}
/**
* Load meta data.
*
* Retrieve the post CSS file meta data.
*
* @since 1.2.0
* @access protected
*
* @return array Post CSS file meta data.
*/
protected function load_meta() {
return get_post_meta( $this->post_id, static::META_KEY, true );
}
/**
* Update meta data.
*
* Update the global CSS file meta data.
*
* @since 1.2.0
* @access protected
*
* @param array $meta New meta data.
*/
protected function update_meta( $meta ) {
update_post_meta( $this->post_id, static::META_KEY, $meta );
}
/**
* Delete meta.
*
* Delete the file meta data.
*
* @since 2.1.0
* @access protected
*/
protected function delete_meta() {
delete_post_meta( $this->post_id, static::META_KEY );
}
/**
* Get post data.
*
* Retrieve raw post data from the database.
*
* @since 1.9.0
* @access protected
*
* @return array Post data.
*/
protected function get_data() {
$document = Plugin::$instance->documents->get( $this->post_id );
return $document ? $document->get_elements_data() : [];
}
/**
* Render CSS.
*
* Parse the CSS for all the elements.
*
* @since 1.2.0
* @access protected
*/
protected function render_css() {
$data = $this->get_data();
if ( ! empty( $data ) ) {
foreach ( $data as $element_data ) {
$element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
if ( ! $element ) {
continue;
}
$this->render_styles( $element );
}
}
}
/**
* Enqueue CSS.
*
* Enqueue the post CSS file in Elementor.
*
* This method ensures that the post was actually built with elementor before
* enqueueing the post CSS file.
*
* @since 1.2.2
* @access public
*/
public function enqueue() {
if ( ! Plugin::$instance->db->is_built_with_elementor( $this->post_id ) ) {
return;
}
parent::enqueue();
}
/**
* Add controls-stack style rules.
*
* Parse the CSS for all the elements inside any given controls stack.
*
* This method recursively renders the CSS for all the child elements in the stack.
*
* @since 1.6.0
* @access public
*
* @param Controls_Stack $controls_stack The controls stack.
* @param array $controls Controls array.
* @param array $values Values array.
* @param array $placeholders Placeholders.
* @param array $replacements Replacements.
* @param array $all_controls All controls.
*/
public function add_controls_stack_style_rules( Controls_Stack $controls_stack, array $controls, array $values, array $placeholders, array $replacements, array $all_controls = null ) {
parent::add_controls_stack_style_rules( $controls_stack, $controls, $values, $placeholders, $replacements, $all_controls );
if ( $controls_stack instanceof Element_Base ) {
foreach ( $controls_stack->get_children() as $child_element ) {
$this->render_styles( $child_element );
}
}
}
/**
* Get enqueue dependencies.
*
* Retrieve the name of the stylesheet used by `wp_enqueue_style()`.
*
* @since 1.2.0
* @access protected
*
* @return array Name of the stylesheet.
*/
protected function get_enqueue_dependencies() {
return [ 'elementor-frontend' ];
}
/**
* Get inline dependency.
*
* Retrieve the name of the stylesheet used by `wp_add_inline_style()`.
*
* @since 1.2.0
* @access protected
*
* @return string Name of the stylesheet.
*/
protected function get_inline_dependency() {
return 'elementor-frontend';
}
/**
* Get file handle ID.
*
* Retrieve the handle ID for the post CSS file.
*
* @since 1.2.0
* @access protected
*
* @return string CSS file handle ID.
*/
protected function get_file_handle_id() {
return 'elementor-post-' . $this->post_id;
}
/**
* Render styles.
*
* Parse the CSS for any given element.
*
* @since 1.2.0
* @access protected
*
* @param Element_Base $element The element.
*/
protected function render_styles( Element_Base $element ) {
/**
* Before element parse CSS.
*
* Fires before the CSS of the element is parsed.
*
* @since 1.2.0
*
* @param Post $this The post CSS file.
* @param Element_Base $element The element.
*/
do_action( 'elementor/element/before_parse_css', $this, $element );
$element_settings = $element->get_settings();
$this->add_controls_stack_style_rules( $element, $this->get_style_controls( $element, null, $element->get_parsed_dynamic_settings() ), $element_settings, [ '{{ID}}', '{{WRAPPER}}' ], [ $element->get_id(), $this->get_element_unique_selector( $element ) ] );
/**
* After element parse CSS.
*
* Fires after the CSS of the element is parsed.
*
* @since 1.2.0
*
* @param Post $this The post CSS file.
* @param Element_Base $element The element.
*/
do_action( 'elementor/element/parse_css', $this, $element );
}
}
@@ -0,0 +1,162 @@
<?php
namespace Elementor\Core\Files;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\Assets\Files_Upload_Handler;
use Elementor\Core\Files\Assets\Json\Json_Handler;
use Elementor\Core\Files\Assets\Svg\Svg_Handler;
use Elementor\Core\Files\CSS\Global_CSS;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Core\Responsive\Files\Frontend;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor files manager.
*
* Elementor files manager handler class is responsible for creating files.
*
* @since 1.2.0
*/
class Manager {
private $files = [];
/**
* Files manager constructor.
*
* Initializing the Elementor files manager.
*
* @since 1.2.0
* @access public
*/
public function __construct() {
$this->register_actions();
new Svg_Handler();
new Json_Handler();
}
public function get( $class, $args ) {
$id = $class . '-' . wp_json_encode( $args );
if ( ! isset( $this->files[ $id ] ) ) {
// Create an instance from dynamic args length.
$reflection_class = new \ReflectionClass( $class );
$this->files[ $id ] = $reflection_class->newInstanceArgs( $args );
}
return $this->files[ $id ];
}
/**
* On post delete.
*
* Delete post CSS immediately after a post is deleted from the database.
*
* Fired by `deleted_post` action.
*
* @since 1.2.0
* @access public
*
* @param string $post_id Post ID.
*/
public function on_delete_post( $post_id ) {
if ( ! Utils::is_post_support( $post_id ) ) {
return;
}
$css_file = Post_CSS::create( $post_id );
$css_file->delete();
}
/**
* On export post meta.
*
* When exporting data using WXR, skip post CSS file meta key. This way the
* export won't contain the post CSS file data used by Elementor.
*
* Fired by `wxr_export_skip_postmeta` filter.
*
* @since 1.2.0
* @access public
*
* @param bool $skip Whether to skip the current post meta.
* @param string $meta_key Current meta key.
*
* @return bool Whether to skip the post CSS meta.
*/
public function on_export_post_meta( $skip, $meta_key ) {
if ( Post_CSS::META_KEY === $meta_key ) {
$skip = true;
}
return $skip;
}
/**
* Clear cache.
*
* Delete all meta containing files data. And delete the actual
* files from the upload directory.
*
* @since 1.2.0
* @access public
*/
public function clear_cache() {
delete_post_meta_by_key( Post_CSS::META_KEY );
delete_option( Global_CSS::META_KEY );
delete_option( Frontend::META_KEY );
// Delete files.
$path = Base::get_base_uploads_dir() . Base::DEFAULT_FILES_DIR . '*';
foreach ( glob( $path ) as $file_path ) {
unlink( $file_path );
}
/**
* Elementor clear files.
*
* Fires after Elementor clears files
*
* @since 2.1.0
*/
do_action( 'elementor/core/files/clear_cache' );
}
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'enable_unfiltered_files_upload', [ $this, 'ajax_unfiltered_files_upload' ] );
}
public function ajax_unfiltered_files_upload() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
update_option( Files_Upload_Handler::OPTION_KEY, 1 );
}
/**
* Register actions.
*
* Register filters and actions for the files manager.
*
* @since 1.2.0
* @access private
*/
private function register_actions() {
add_action( 'deleted_post', [ $this, 'on_delete_post' ] );
// Ajax.
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
add_filter( 'wxr_export_skip_postmeta', [ $this, 'on_export_post_meta' ], 10, 2 );
}
}
@@ -0,0 +1,142 @@
<?php
namespace Elementor\Core\Frontend;
use Elementor\Core\Frontend\RenderModes\Render_Mode_Base;
use Elementor\Core\Frontend\RenderModes\Render_Mode_Normal;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Render_Mode_Manager {
const QUERY_STRING_PARAM_NAME = 'render_mode';
const QUERY_STRING_POST_ID = 'post_id';
const QUERY_STRING_NONCE_PARAM_NAME = 'render_mode_nonce';
const NONCE_ACTION_PATTERN = 'render_mode_{post_id}';
/**
* @var Render_Mode_Base
*/
private $current;
/**
* @var Render_Mode_Base[]
*/
private $render_modes = [];
/**
* @param $post_id
* @param $render_mode_name
*
* @return string
*/
public static function get_base_url( $post_id, $render_mode_name ) {
return add_query_arg( [
self::QUERY_STRING_POST_ID => $post_id,
self::QUERY_STRING_PARAM_NAME => $render_mode_name,
self::QUERY_STRING_NONCE_PARAM_NAME => wp_create_nonce( self::get_nonce_action( $post_id ) ),
'ver' => time(),
], get_permalink( $post_id ) );
}
/**
* @param $post_id
*
* @return string
*/
public static function get_nonce_action( $post_id ) {
return str_replace( '{post_id}', $post_id, self::NONCE_ACTION_PATTERN );
}
/**
* Register a new render mode into the render mode manager.
*
* @param $class_name
*
* @return $this
* @throws \Exception
*/
public function register_render_mode( $class_name ) {
if ( ! is_subclass_of( $class_name, Render_Mode_Base::class ) ) {
throw new \Exception( "'{$class_name}' must extends 'Render_Mode_Base'" );
}
$this->render_modes[ $class_name::get_name() ] = $class_name;
return $this;
}
/**
* Get the current render mode.
*
* @return Render_Mode_Base
*/
public function get_current() {
return $this->current;
}
/**
* Set render mode.
*
* @return $this
*/
private function set_current_render_mode() {
$post_id = null;
$key = null;
$nonce = null;
if ( isset( $_GET[ self::QUERY_STRING_POST_ID ] ) ) {
$post_id = $_GET[ self::QUERY_STRING_POST_ID ]; // phpcs:ignore -- Nonce will be checked next line.
}
if ( isset( $_GET[ self::QUERY_STRING_NONCE_PARAM_NAME ] ) ) {
$nonce = $_GET[ self::QUERY_STRING_NONCE_PARAM_NAME ]; // phpcs:ignore -- Nonce will be checked next line.
}
if ( isset( $_GET[ self::QUERY_STRING_PARAM_NAME ] ) ) {
$key = $_GET[ self::QUERY_STRING_PARAM_NAME ]; // phpcs:ignore -- Nonce will be checked next line.
}
if (
$post_id &&
$nonce &&
wp_verify_nonce( $nonce, self::get_nonce_action( $post_id ) ) &&
$key &&
array_key_exists( $key, $this->render_modes )
) {
$this->current = new $this->render_modes[ $key ]( $post_id );
} else {
$this->current = new Render_Mode_Normal( $post_id );
}
return $this;
}
/**
* Add actions base on the current render.
*
* @throws \Requests_Exception_HTTP_403
*/
private function add_current_actions() {
if ( ! $this->current->get_permissions_callback() ) {
throw new \Requests_Exception_HTTP_403();
}
// Run when 'template-redirect' actually because the the class is instantiate when 'template-redirect' run.
$this->current->prepare_render();
}
/**
* Render_Mode_Manager constructor.
*
* @throws \Exception
*/
public function __construct() {
$this->register_render_mode( Render_Mode_Normal::class );
do_action( 'elementor/frontend/render_mode/register', $this );
$this->set_current_render_mode();
$this->add_current_actions();
}
}
@@ -0,0 +1,110 @@
<?php
namespace Elementor\Core\Frontend\RenderModes;
use Elementor\Plugin;
use Elementor\Core\Base\Document;
use Elementor\Core\Frontend\Render_Mode_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Render_Mode_Base {
const ENQUEUE_SCRIPTS_PRIORITY = 10;
const ENQUEUE_STYLES_PRIORITY = 10;
/**
* @var int
*/
protected $post_id;
/**
* @var Document
*/
protected $document;
/**
* Render_Mode_Base constructor.
*
* @param $post_id
*/
public function __construct( $post_id ) {
$this->post_id = intval( $post_id );
}
/**
* Returns the key name of the class.
*
* @return string
* @throws \Exception
*/
public static function get_name() {
throw new \Exception( 'You must implements `get_name` static method in ' . static::class );
}
/**
* @param $post_id
*
* @return string
* @throws \Exception
*/
public static function get_url( $post_id ) {
return Render_Mode_Manager::get_base_url( $post_id, static::get_name() );
}
/**
* Runs before the render, by default load scripts and styles.
*/
public function prepare_render() {
add_action( 'wp_enqueue_scripts', function () {
$this->enqueue_scripts();
}, static::ENQUEUE_SCRIPTS_PRIORITY );
add_action( 'wp_enqueue_scripts', function () {
$this->enqueue_styles();
}, static::ENQUEUE_STYLES_PRIORITY );
}
/**
* By default do not do anything.
*/
protected function enqueue_scripts() {
//
}
/**
* By default do not do anything.
*/
protected function enqueue_styles() {
//
}
/**
* Check if the current user has permissions for the current render mode.
*
* @return bool
*/
public function get_permissions_callback() {
return $this->get_document()->is_editable_by_current_user();
}
/**
* Checks if the current render mode is static render, By default returns false.
*
* @return bool
*/
public function is_static() {
return false;
}
/**
* @return Document
*/
public function get_document() {
if ( ! $this->document ) {
$this->document = Plugin::$instance->documents->get( $this->post_id );
}
return $this->document;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Elementor\Core\Frontend\RenderModes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Render_Mode_Normal extends Render_Mode_Base {
/**
* @return string
*/
public static function get_name() {
return 'normal';
}
/**
* Anyone can access the normal render mode.
*
* @return bool
*/
public function get_permissions_callback() {
return true;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Elementor\Core\Kits\Controls;
use Elementor\Control_Repeater;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Repeater extends Control_Repeater {
const CONTROL_TYPE = 'global-style-repeater';
/**
* Get control type.
*
* Retrieve the control type, in this case `global-style-repeater`.
*
* @since 3.0.0
* @access public
*
* @return string Control type.
*/
public function get_type() {
return self::CONTROL_TYPE;
}
/**
* Get repeater control default settings.
*
* Retrieve the default settings of the repeater control. Used to return the
* default settings while initializing the repeater control.
*
* @since 3.0.0
* @access protected
*
* @return array Control default settings.
*/
protected function get_default_settings() {
$settings = parent::get_default_settings();
$settings['item_actions']['duplicate'] = false;
$settings['item_actions']['sort'] = false;
return $settings;
}
/**
* Render repeater control output in the editor.
*
* Used to generate the control HTML in the editor using Underscore JS
* template. The variables for the class are available using `data` JS
* object.
*
* @since 3.0.0
* @access public
*/
public function content_template() {
?>
<div class="elementor-repeater-fields-wrapper"></div>
<# if ( itemActions.add ) { #>
<div class="elementor-button-wrapper">
<button class="elementor-button elementor-button-default elementor-repeater-add" type="button">
<i class="eicon-plus" aria-hidden="true"></i><span class="elementor-repeater__add-button__text">{{{ addButtonText }}}</span>
</button>
</div>
<# } #>
<?php
}
}
@@ -0,0 +1,156 @@
<?php
namespace Elementor\Core\Kits\Documents;
use Elementor\Core\DocumentTypes\PageBase;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Core\Kits\Documents\Tabs;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Core\Settings\Page\Manager as PageManager;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Kit extends PageBase {
/**
* @var Tabs\Tab_Base[]
*/
private $tabs;
public function __construct( array $data = [] ) {
parent::__construct( $data );
$this->tabs = [
'global-colors' => new Tabs\Global_Colors( $this ),
'global-typography' => new Tabs\Global_Typography( $this ),
'theme-style-typography' => new Tabs\Theme_Style_Typography( $this ),
'theme-style-buttons' => new Tabs\Theme_Style_Buttons( $this ),
'theme-style-images' => new Tabs\Theme_Style_Images( $this ),
'theme-style-form-fields' => new Tabs\Theme_Style_Form_Fields( $this ),
'settings-site-identity' => new Tabs\Settings_Site_Identity( $this ),
'settings-background' => new Tabs\Settings_Background( $this ),
'settings-layout' => new Tabs\Settings_Layout( $this ),
'settings-lightbox' => new Tabs\Settings_Lightbox( $this ),
'settings-custom-css' => new Tabs\Settings_Custom_CSS( $this ),
];
}
public static function get_properties() {
$properties = parent::get_properties();
$properties['has_elements'] = false;
$properties['show_in_finder'] = false;
$properties['show_on_admin_bar'] = false;
$properties['edit_capability'] = 'edit_theme_options';
$properties['support_kit'] = true;
return $properties;
}
public function get_name() {
return 'kit';
}
public static function get_title() {
return __( 'Kit', 'elementor' );
}
protected function get_have_a_look_url() {
return '';
}
public static function get_editor_panel_config() {
$config = parent::get_editor_panel_config();
$config['default_route'] = 'panel/global/menu';
$config['needHelpUrl'] = 'https://go.elementor.com/global-settings';
return $config;
}
public function get_css_wrapper_selector() {
return '.elementor-kit-' . $this->get_main_id();
}
public function save( $data ) {
$saved = parent::save( $data );
if ( ! $saved ) {
return false;
}
// Should set is_saving to true, to avoid infinite loop when updating
// settings like: 'site_name" or "site_description".
$this->set_is_saving( true );
foreach ( $this->tabs as $tab ) {
$tab->on_save( $data );
}
$this->set_is_saving( false );
// When deleting a global color or typo, the css variable still exists in the frontend
// but without any value and it makes the element to be un styled even if there is a default style for the base element,
// for that reason this method removes css files of the entire site.
Plugin::instance()->files_manager->clear_cache();
return $saved;
}
/**
* @since 2.0.0
* @access protected
*/
protected function _register_controls() {
$this->register_document_controls();
foreach ( $this->tabs as $tab ) {
$tab->register_controls();
}
}
protected function get_post_statuses() {
return [
'draft' => sprintf( '%s (%s)', __( 'Disabled', 'elementor' ), __( 'Draft', 'elementor' ) ),
'publish' => __( 'Published', 'elementor' ),
];
}
public function add_repeater_row( $control_id, $item ) {
$meta_key = PageManager::META_KEY;
$document_settings = $this->get_meta( $meta_key );
if ( ! $document_settings ) {
$document_settings = [];
}
if ( ! isset( $document_settings[ $control_id ] ) ) {
$document_settings[ $control_id ] = [];
}
$document_settings[ $control_id ][] = $item;
$page_settings_manager = SettingsManager::get_settings_managers( 'page' );
$page_settings_manager->save_settings( $document_settings, $this->get_id() );
/** @var Kit $autosave **/
$autosave = $this->get_autosave();
if ( $autosave ) {
$autosave->add_repeater_row( $control_id, $item );
}
// Remove Post CSS.
$post_css = Post_CSS::create( $this->post->ID );
$post_css->delete();
// Refresh Cache.
Plugin::$instance->documents->get( $this->post->ID, false );
$post_css = Post_CSS::create( $this->post->ID );
$post_css->enqueue();
}
}
@@ -0,0 +1,109 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Core\Kits\Controls\Repeater as Global_Style_Repeater;
use Elementor\Repeater;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Global_Colors extends Tab_Base {
const COLOR_PRIMARY = 'globals/colors?id=primary';
const COLOR_SECONDARY = 'globals/colors?id=secondary';
const COLOR_TEXT = 'globals/colors?id=text';
const COLOR_ACCENT = 'globals/colors?id=accent';
public function get_id() {
return 'global-colors';
}
public function get_title() {
return __( 'Global Colors', 'elementor' );
}
protected function register_tab_controls() {
$this->start_controls_section(
'section_global_colors',
[
'label' => __( 'Global Colors', 'elementor' ),
'tab' => $this->get_id(),
]
);
$repeater = new Repeater();
$repeater->add_control(
'title',
[
'type' => Controls_Manager::TEXT,
'label_block' => true,
'required' => true,
]
);
// Color Value
$repeater->add_control(
'color',
[
'type' => Controls_Manager::COLOR,
'label_block' => true,
'dynamic' => [],
'selectors' => [
'{{WRAPPER}}' => '--e-global-color-{{_id.VALUE}}: {{VALUE}}',
],
'global' => [
'active' => false,
],
]
);
$default_colors = [
[
'_id' => 'primary',
'title' => __( 'Primary', 'elementor' ),
'color' => '#6EC1E4',
],
[
'_id' => 'secondary',
'title' => __( 'Secondary', 'elementor' ),
'color' => '#54595F',
],
[
'_id' => 'text',
'title' => __( 'Text', 'elementor' ),
'color' => '#7A7A7A',
],
[
'_id' => 'accent',
'title' => __( 'Accent', 'elementor' ),
'color' => '#61CE70',
],
];
$this->add_control(
'system_colors',
[
'type' => Global_Style_Repeater::CONTROL_TYPE,
'fields' => $repeater->get_controls(),
'default' => $default_colors,
'item_actions' => [
'add' => false,
'remove' => false,
],
]
);
$this->add_control(
'custom_colors',
[
'type' => Global_Style_Repeater::CONTROL_TYPE,
'fields' => $repeater->get_controls(),
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,174 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Core\Kits\Controls\Repeater as Global_Style_Repeater;
use Elementor\Group_Control_Typography;
use Elementor\Repeater;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Global_Typography extends Tab_Base {
const TYPOGRAPHY_PRIMARY = 'globals/typography?id=primary';
const TYPOGRAPHY_SECONDARY = 'globals/typography?id=secondary';
const TYPOGRAPHY_TEXT = 'globals/typography?id=text';
const TYPOGRAPHY_ACCENT = 'globals/typography?id=accent';
const TYPOGRAPHY_NAME = 'typography';
const TYPOGRAPHY_GROUP_PREFIX = self::TYPOGRAPHY_NAME . '_';
public function get_id() {
return 'global-typography';
}
public function get_title() {
return __( 'Global Fonts', 'elementor' );
}
protected function register_tab_controls() {
$this->start_controls_section(
'section_text_style',
[
'label' => __( 'Global Fonts', 'elementor' ),
'tab' => $this->get_id(),
]
);
$repeater = new Repeater();
$repeater->add_control(
'title',
[
'type' => Controls_Manager::TEXT,
'label_block' => true,
'required' => true,
]
);
$repeater->add_group_control(
Group_Control_Typography::get_type(),
[
'name' => self::TYPOGRAPHY_NAME,
'label' => '',
'global' => [
'active' => false,
],
'fields_options' => [
'font_family' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-font-family: "{{VALUE}}"',
],
],
'font_size' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-font-size: {{SIZE}}{{UNIT}}',
],
],
'font_weight' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-font-weight: {{VALUE}}',
],
],
'text_transform' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-text-transform: {{VALUE}}',
],
],
'font_style' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-font-style: {{VALUE}}',
],
],
'text_decoration' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-text-decoration: {{VALUE}}',
],
],
'line_height' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-line-height: {{SIZE}}{{UNIT}}',
],
],
'letter_spacing' => [
'selectors' => [
'{{SELECTOR}}' => '--e-global-typography-{{external._id.VALUE}}-letter-spacing: {{SIZE}}{{UNIT}}',
],
],
],
]
);
$typography_key = self::TYPOGRAPHY_GROUP_PREFIX . 'typography';
$font_family_key = self::TYPOGRAPHY_GROUP_PREFIX . 'font_family';
$font_weight_key = self::TYPOGRAPHY_GROUP_PREFIX . 'font_weight';
$default_typography = [
[
'_id' => 'primary',
'title' => __( 'Primary', 'elementor' ),
$typography_key => 'custom',
$font_family_key => 'Roboto',
$font_weight_key => '600',
],
[
'_id' => 'secondary',
'title' => __( 'Secondary', 'elementor' ),
$typography_key => 'custom',
$font_family_key => 'Roboto Slab',
$font_weight_key => '400',
],
[
'_id' => 'text',
'title' => __( 'Text', 'elementor' ),
$typography_key => 'custom',
$font_family_key => 'Roboto',
$font_weight_key => '400',
],
[
'_id' => 'accent',
'title' => __( 'Accent', 'elementor' ),
$typography_key => 'custom',
$font_family_key => 'Roboto',
$font_weight_key => '500',
],
];
$this->add_control(
'system_typography',
[
'type' => Global_Style_Repeater::CONTROL_TYPE,
'fields' => $repeater->get_controls(),
'default' => $default_typography,
'item_actions' => [
'add' => false,
'remove' => false,
],
]
);
$this->add_control(
'custom_typography',
[
'type' => Global_Style_Repeater::CONTROL_TYPE,
'fields' => $repeater->get_controls(),
]
);
$this->add_control(
'default_generic_fonts',
[
'label' => __( 'Fallback Font Family', 'elementor' ),
'type' => Controls_Manager::TEXT,
'default' => 'Sans-serif',
'description' => __( 'The list of fonts used if the chosen font is not available.', 'elementor' ),
'label_block' => true,
'separator' => 'before',
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,62 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Group_Control_Background;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Settings_Background extends Tab_Base {
public function get_id() {
return 'settings-background';
}
public function get_title() {
return __( 'Background', 'elementor' );
}
protected function register_tab_controls() {
$this->start_controls_section(
'section_background',
[
'label' => $this->get_title(),
'tab' => $this->get_id(),
]
);
$this->add_group_control(
Group_Control_Background::get_type(),
[
'name' => 'body_background',
'types' => [ 'classic', 'gradient' ],
'selector' => '{{WRAPPER}}',
'fields_options' => [
'background' => [
'frontend_available' => true,
],
'color' => [
'dynamic' => [],
],
'color_b' => [
'dynamic' => [],
],
],
]
);
$this->add_control(
'mobile_browser_background',
[
'label' => __( 'Mobile Browser Background', 'elementor' ),
'type' => Controls_Manager::COLOR,
'description' => __( 'The `theme-color` meta tag will only be available in supported browsers and devices.', 'elementor' ),
'separator' => 'before',
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,23 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Settings_Custom_CSS extends Tab_Base {
public function get_id() {
return 'settings-custom-css';
}
public function get_title() {
return __( 'Custom CSS', 'elementor' );
}
protected function register_tab_controls() {
Plugin::$instance->controls_manager->add_custom_css_controls( $this->parent, $this->get_id() );
}
}
@@ -0,0 +1,204 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\DB;
use Elementor\Plugin;
use Elementor\Controls_Manager;
use Elementor\Core\Responsive\Responsive;
use Elementor\Modules\PageTemplates\Module as PageTemplatesModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Settings_Layout extends Tab_Base {
public function get_id() {
return 'settings-layout';
}
public function get_title() {
return __( 'Layout', 'elementor' );
}
protected function register_tab_controls() {
$default_breakpoints = Responsive::get_default_breakpoints();
$this->start_controls_section(
'section_' . $this->get_id(),
[
'label' => __( 'Layout Settings', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_responsive_control(
'container_width',
[
'label' => __( 'Content Width', 'elementor' ) . ' (px)',
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => '1140',
],
'tablet_default' => [
'size' => $default_breakpoints['lg'],
],
'mobile_default' => [
'size' => $default_breakpoints['md'],
],
'range' => [
'px' => [
'min' => 300,
'max' => 1500,
'step' => 10,
],
],
'description' => __( 'Sets the default width of the content area (Default: 1140)', 'elementor' ),
'selectors' => [
'.elementor-section.elementor-section-boxed > .elementor-container' => 'max-width: {{SIZE}}{{UNIT}}',
],
]
);
$this->add_control(
'space_between_widgets',
[
'label' => __( 'Widgets Space', 'elementor' ) . ' (px)',
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => 20,
],
'range' => [
'px' => [
'min' => 0,
'max' => 40,
],
],
'placeholder' => '20',
'description' => __( 'Sets the default space between widgets (Default: 20)', 'elementor' ),
'selectors' => [
'.elementor-widget:not(:last-child)' => 'margin-bottom: {{SIZE}}{{UNIT}}',
],
]
);
$this->add_control(
'page_title_selector',
[
'label' => __( 'Page Title Selector', 'elementor' ),
'type' => Controls_Manager::TEXT,
'default' => 'h1.entry-title',
'placeholder' => 'h1.entry-title',
'description' => __( 'Elementor lets you hide the page title. This works for themes that have "h1.entry-title" selector. If your theme\'s selector is different, please enter it above.', 'elementor' ),
'label_block' => true,
'selectors' => [
// Hack to convert the value into a CSS selector.
'' => '}{{VALUE}}{display: var(--page-title-display)',
],
]
);
$this->add_control(
'stretched_section_container',
[
'label' => __( 'Stretched Section Fit To', 'elementor' ),
'type' => Controls_Manager::TEXT,
'placeholder' => 'body',
'description' => __( 'Enter parent element selector to which stretched sections will fit to (e.g. #primary / .wrapper / main etc). Leave blank to fit to page width.', 'elementor' ),
'label_block' => true,
'frontend_available' => true,
]
);
/**
* @var PageTemplatesModule $page_templates_module
*/
$page_templates_module = Plugin::$instance->modules_manager->get_modules( 'page-templates' );
$page_templates = $page_templates_module->add_page_templates( [], null, null );
$page_template_control_options = [
'label' => __( 'Default Page Layout', 'elementor' ),
'options' => [
// This is here because the "Theme" string is different than the default option's string.
'default' => __( 'Theme', 'elementor' ),
] + $page_templates,
];
$page_templates_module->add_template_controls( $this->parent, 'default_page_template', $page_template_control_options );
$this->end_controls_section();
$this->start_controls_section(
'section_breakpoints',
[
'label' => __( 'Breakpoints', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_control(
'breakpoint_md_heading',
[
'label' => __( 'Mobile', 'elementor' ),
'type' => Controls_Manager::HEADING,
]
);
$this->add_control(
Responsive::BREAKPOINT_OPTION_PREFIX . 'md',
[
'label' => __( 'Breakpoint', 'elementor' ) . ' (px)',
'type' => Controls_Manager::NUMBER,
'min' => $default_breakpoints['sm'] + 1,
'max' => $default_breakpoints['lg'] - 1,
'default' => $default_breakpoints['md'],
'placeholder' => $default_breakpoints['md'],
/* translators: %d: Breakpoint value */
'desc' => sprintf( __( 'Sets the breakpoint between tablet and mobile devices. Below this breakpoint mobile layout will appear (Default: %dpx).', 'elementor' ), $default_breakpoints['md'] ),
]
);
$this->add_control(
'breakpoint_lg_heading',
[
'label' => __( 'Tablet', 'elementor' ),
'type' => Controls_Manager::HEADING,
]
);
$this->add_control(
Responsive::BREAKPOINT_OPTION_PREFIX . 'lg',
[
'label' => __( 'Breakpoint', 'elementor' ) . ' (px)',
'type' => Controls_Manager::NUMBER,
'min' => $default_breakpoints['md'] + 1,
'max' => $default_breakpoints['xl'] - 1,
'default' => $default_breakpoints['lg'],
'placeholder' => $default_breakpoints['lg'],
/* translators: %d: Breakpoint value */
'desc' => sprintf( __( 'Sets the breakpoint between desktop and tablet devices. Below this breakpoint tablet layout will appear (Default: %dpx).', 'elementor' ), $default_breakpoints['lg'] ),
]
);
$this->end_controls_section();
}
public function on_save( $data ) {
if ( ! isset( $data['settings'] ) || DB::STATUS_PUBLISH !== $data['settings']['post_status'] ) {
return;
}
$should_compile_css = false;
foreach ( Responsive::get_editable_breakpoints() as $breakpoint_key => $breakpoint ) {
$setting_key = "viewport_{$breakpoint_key}";
if ( isset( $data['settings'][ $setting_key ] ) ) {
$should_compile_css = true;
}
}
if ( $should_compile_css ) {
Responsive::compile_stylesheet_templates();
}
}
}
@@ -0,0 +1,185 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Settings_Lightbox extends Tab_Base {
public function get_id() {
return 'settings-lightbox';
}
public function get_title() {
return __( 'Lightbox', 'elementor' );
}
protected function register_tab_controls() {
$this->start_controls_section(
'section_' . $this->get_id(),
[
'label' => $this->get_title(),
'tab' => $this->get_id(),
]
);
$this->add_control(
'global_image_lightbox',
[
'label' => __( 'Image Lightbox', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
'description' => __( 'Open all image links in a lightbox popup window. The lightbox will automatically work on any link that leads to an image file.', 'elementor' ),
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_enable_counter',
[
'label' => __( 'Counter', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_enable_fullscreen',
[
'label' => __( 'Fullscreen', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_enable_zoom',
[
'label' => __( 'Zoom', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_enable_share',
[
'label' => __( 'Share', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_title_src',
[
'label' => __( 'Title', 'elementor' ),
'type' => Controls_Manager::SELECT,
'options' => [
'' => __( 'None', 'elementor' ),
'title' => __( 'Title', 'elementor' ),
'caption' => __( 'Caption', 'elementor' ),
'alt' => __( 'Alt', 'elementor' ),
'description' => __( 'Description', 'elementor' ),
],
'default' => 'title',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_description_src',
[
'label' => __( 'Description', 'elementor' ),
'type' => Controls_Manager::SELECT,
'options' => [
'' => __( 'None', 'elementor' ),
'title' => __( 'Title', 'elementor' ),
'caption' => __( 'Caption', 'elementor' ),
'alt' => __( 'Alt', 'elementor' ),
'description' => __( 'Description', 'elementor' ),
],
'default' => 'description',
'frontend_available' => true,
]
);
$this->add_control(
'lightbox_color',
[
'label' => __( 'Background Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'selectors' => [
'.elementor-lightbox' => 'background-color: {{VALUE}}',
],
]
);
$this->add_control(
'lightbox_ui_color',
[
'label' => __( 'UI Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'selectors' => [
'.elementor-lightbox' => '--lightbox-ui-color: {{VALUE}}',
],
]
);
$this->add_control(
'lightbox_ui_color_hover',
[
'label' => __( 'UI Hover Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'selectors' => [
'.elementor-lightbox' => '--lightbox-ui-color-hover: {{VALUE}}',
],
]
);
$this->add_control(
'lightbox_text_color',
[
'label' => __( 'Text Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'selectors' => [
'.elementor-lightbox' => '--lightbox-text-color: {{VALUE}}',
],
]
);
$this->add_control(
'lightbox_icons_size',
[
'label' => __( 'Toolbar Icons Size', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'selectors' => [
'.elementor-lightbox' => '--lightbox-header-icons-size: {{SIZE}}{{UNIT}}',
],
'separator' => 'before',
]
);
$this->add_control(
'lightbox_slider_icons_size',
[
'label' => __( 'Navigation Icons Size', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'selectors' => [
'.elementor-lightbox' => '--lightbox-navigation-icons-size: {{SIZE}}{{UNIT}}',
],
'separator' => 'before',
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,122 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\DB;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Settings_Site_Identity extends Tab_Base {
public function get_id() {
return 'settings-site-identity';
}
public function get_title() {
return __( 'Site Identity', 'elementor' );
}
protected function register_tab_controls() {
$custom_logo_id = get_theme_mod( 'custom_logo' );
$custom_logo_src = wp_get_attachment_image_src( $custom_logo_id, 'full' );
$site_icon_id = get_option( 'site_icon' );
$site_icon_src = wp_get_attachment_image_src( $site_icon_id, 'full' );
$this->start_controls_section(
'section_' . $this->get_id(),
[
'label' => $this->get_title(),
'tab' => $this->get_id(),
]
);
$this->add_control(
$this->get_id() . '_refresh_notice',
[
'type' => Controls_Manager::RAW_HTML,
'raw' => __( 'Changes will be reflected in the preview only after the page reloads.', 'elementor' ),
'content_classes' => 'elementor-panel-alert elementor-panel-alert-info',
]
);
$this->add_control(
'site_name',
[
'label' => __( 'Site Name', 'elementor' ),
'default' => get_option( 'blogname' ),
'placeholder' => __( 'Choose name', 'elementor' ),
'label_block' => true,
]
);
$this->add_control(
'site_description',
[
'label' => __( 'Site Description', 'elementor' ),
'default' => get_option( 'blogdescription' ),
'placeholder' => __( 'Choose description', 'elementor' ),
'label_block' => true,
]
);
$this->add_control(
'site_logo',
[
'label' => __( 'Site Logo', 'elementor' ),
'type' => Controls_Manager::MEDIA,
'default' => [
'id' => $custom_logo_id,
'url' => $custom_logo_src ? $custom_logo_src[0] : '',
],
'description' => __( 'Suggested image dimensions: 350 × 100 pixels.', 'elementor' ),
]
);
$this->add_control(
'site_favicon',
[
'label' => __( 'Site Favicon', 'elementor' ),
'type' => Controls_Manager::MEDIA,
'default' => [
'id' => $site_icon_id,
'url' => $site_icon_src ? $site_icon_src[0] : '',
],
'description' => __( 'Suggested favicon dimensions: 512 × 512 pixels.', 'elementor' ),
]
);
$this->end_controls_section();
}
public function on_save( $data ) {
if (
! isset( $data['settings'] ) ||
DB::STATUS_PUBLISH !== $data['settings']['post_status'] ||
// Should check for the current action to avoid infinite loop
// when updating options like: "blogname" and "blogdescription".
strpos( current_action(), 'update_option_' ) === 0
) {
return;
}
if ( isset( $data['settings']['site_name'] ) ) {
update_option( 'blogname', $data['settings']['site_name'] );
}
if ( isset( $data['settings']['site_description'] ) ) {
update_option( 'blogdescription', $data['settings']['site_description'] );
}
if ( isset( $data['settings']['site_logo'] ) ) {
set_theme_mod( 'custom_logo', $data['settings']['site_logo']['id'] );
}
if ( isset( $data['settings']['site_favicon'] ) ) {
update_option( 'site_icon', $data['settings']['site_favicon']['id'] );
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Core\Kits\Manager;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\Sub_Controls_Stack;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Tab_Base extends Sub_Controls_Stack {
/**
* @var Kit
*/
protected $parent;
abstract protected function register_tab_controls();
public function register_controls() {
$this->register_tab();
$this->register_tab_controls();
}
public function on_save( $data ) {}
protected function register_tab() {
Controls_Manager::add_tab( $this->get_id(), $this->get_title() );
}
protected function add_default_globals_notice() {
// Get the current section config (array - section id and tab) to use for creating a unique control ID and name
$current_section = $this->parent->get_current_section();
/** @var Manager $module */
$kits_manager = Plugin::$instance->kits_manager;
if ( $kits_manager->is_custom_colors_enabled() || $kits_manager->is_custom_typography_enabled() ) {
$this->add_control(
$current_section['section'] . '_schemes_notice',
[
'name' => $current_section['section'] . '_schemes_notice',
'type' => Controls_Manager::RAW_HTML,
'raw' => sprintf( __( 'In order for Theme Style to affect all relevant Elementor elements, please disable Default Colors and Fonts from the <a href="%s" target="_blank">Settings Page</a>.', 'elementor' ), Settings::get_url() ),
'content_classes' => 'elementor-panel-alert elementor-panel-alert-warning',
'render_type' => 'ui',
]
);
}
}
}
@@ -0,0 +1,225 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Group_Control_Border;
use Elementor\Group_Control_Box_Shadow;
use Elementor\Group_Control_Text_Shadow;
use Elementor\Group_Control_Typography;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Theme_Style_Buttons extends Tab_Base {
public function get_id() {
return 'theme-style-buttons';
}
public function get_title() {
return __( 'Buttons', 'elementor' );
}
protected function register_tab_controls() {
$button_selectors = [
'{{WRAPPER}} button',
'{{WRAPPER}} input[type="button"]',
'{{WRAPPER}} input[type="submit"]',
'{{WRAPPER}} .elementor-button',
];
$button_hover_selectors = [
'{{WRAPPER}} button:hover',
'{{WRAPPER}} button:focus',
'{{WRAPPER}} input[type="button"]:hover',
'{{WRAPPER}} input[type="button"]:focus',
'{{WRAPPER}} input[type="submit"]:hover',
'{{WRAPPER}} input[type="submit"]:focus',
'{{WRAPPER}} .elementor-button:hover',
'{{WRAPPER}} .elementor-button:focus',
];
$button_selector = implode( ',', $button_selectors );
$button_hover_selector = implode( ',', $button_hover_selectors );
$this->start_controls_section(
'section_buttons',
[
'label' => __( 'Buttons', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_default_globals_notice();
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'button_typography',
'selector' => $button_selector,
]
);
$this->add_group_control(
Group_Control_Text_Shadow::get_type(),
[
'name' => 'button_text_shadow',
'selector' => $button_selector,
]
);
$this->start_controls_tabs( 'tabs_button_style' );
$this->start_controls_tab(
'tab_button_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_control(
'button_text_color',
[
'label' => __( 'Text Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$button_selector => 'color: {{VALUE}};',
],
]
);
$this->add_control(
'button_background_color',
[
'label' => __( 'Background Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$button_selector => 'background-color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => 'button_box_shadow',
'selector' => $button_selector,
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => 'button_border',
'selector' => $button_selector,
'fields_options' => [
'color' => [
'dynamic' => [],
],
],
]
);
$this->add_control(
'button_border_radius',
[
'label' => __( 'Border Radius', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
$button_selector => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->end_controls_tab();
$this->start_controls_tab(
'tab_button_hover',
[
'label' => __( 'Hover', 'elementor' ),
]
);
$this->add_control(
'button_hover_text_color',
[
'label' => __( 'Text Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$button_hover_selector => 'color: {{VALUE}};',
],
]
);
$this->add_control(
'button_hover_background_color',
[
'label' => __( 'Background Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$button_hover_selector => 'background-color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => 'button_hover_box_shadow',
'selector' => $button_hover_selector,
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => 'button_hover_border',
'selector' => $button_hover_selector,
'fields_options' => [
'color' => [
'dynamic' => [],
],
],
]
);
$this->add_control(
'button_hover_border_radius',
[
'label' => __( 'Border Radius', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
$button_hover_selector => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
$this->add_responsive_control(
'button_padding',
[
'label' => __( 'Padding', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', 'em', '%' ],
'selectors' => [
$button_selector => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
'separator' => 'before',
]
);
$this->end_controls_section();
}
}
@@ -0,0 +1,219 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Group_Control_Border;
use Elementor\Group_Control_Box_Shadow;
use Elementor\Group_Control_Typography;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Theme_Style_Form_Fields extends Tab_Base {
public function get_id() {
return 'theme-style-form-fields';
}
public function get_title() {
return __( 'Form Fields', 'elementor' );
}
protected function register_tab_controls() {
$label_selectors = [
'{{WRAPPER}} label',
];
$input_selectors = [
'{{WRAPPER}} input:not([type="button"]):not([type="submit"])',
'{{WRAPPER}} textarea',
'{{WRAPPER}} .elementor-field-textual',
];
$input_focus_selectors = [
'{{WRAPPER}} input:focus:not([type="button"]):not([type="submit"])',
'{{WRAPPER}} textarea:focus',
'{{WRAPPER}} .elementor-field-textual:focus',
];
$label_selector = implode( ',', $label_selectors );
$input_selector = implode( ',', $input_selectors );
$input_focus_selector = implode( ',', $input_focus_selectors );
$this->start_controls_section(
'section_form_fields',
[
'label' => __( 'Form Fields', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_default_globals_notice();
$this->add_control(
'form_label_heading',
[
'type' => Controls_Manager::HEADING,
'label' => __( 'Label', 'elementor' ),
]
);
$this->add_control(
'form_label_color',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$label_selector => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'form_label_typography',
'selector' => $label_selector,
]
);
$this->add_control(
'form_field_heading',
[
'type' => Controls_Manager::HEADING,
'label' => __( 'Field', 'elementor' ),
'separator' => 'before',
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'form_field_typography',
'selector' => $input_selector,
]
);
$this->start_controls_tabs( 'tabs_form_field_style' );
$this->start_controls_tab(
'tab_form_field_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_form_field_state_tab_controls( 'form_field', $input_selector );
$this->end_controls_tab();
$this->start_controls_tab(
'tab_form_field_focus',
[
'label' => __( 'Focus', 'elementor' ),
]
);
$this->add_form_field_state_tab_controls( 'form_field_focus', $input_focus_selector );
$this->add_control(
'form_field_focus_transition_duration',
[
'label' => __( 'Transition Duration', 'elementor' ) . ' (ms)',
'type' => Controls_Manager::SLIDER,
'selectors' => [
$input_selector => 'transition: {{SIZE}}ms',
],
'range' => [
'px' => [
'min' => 0,
'max' => 3000,
],
],
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
$this->add_responsive_control(
'form_field_padding',
[
'label' => __( 'Padding', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', 'em', '%' ],
'selectors' => [
$input_selector => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
'separator' => 'before',
]
);
$this->end_controls_section();
}
private function add_form_field_state_tab_controls( $prefix, $selector ) {
$this->add_control(
$prefix . '_text_color',
[
'label' => __( 'Text Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$selector => 'color: {{VALUE}};',
],
]
);
$this->add_control(
$prefix . '_background_color',
[
'label' => __( 'Background Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$selector => 'background-color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => $prefix . '_box_shadow',
'selector' => $selector,
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => $prefix . '_border',
'selector' => $selector,
'fields_options' => [
'color' => [
'dynamic' => [],
],
],
]
);
$this->add_control(
$prefix . '_border_radius',
[
'label' => __( 'Border Radius', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
$selector => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
}
}
@@ -0,0 +1,211 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Group_Control_Border;
use Elementor\Group_Control_Box_Shadow;
use Elementor\Group_Control_Css_Filter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Theme_Style_Images extends Tab_Base {
public function get_id() {
return 'theme-style-images';
}
public function get_title() {
return __( 'Images', 'elementor' );
}
protected function register_tab_controls() {
$image_selectors = [
'{{WRAPPER}} img',
];
$image_hover_selectors = [
'{{WRAPPER}} img:hover',
];
$image_selectors = implode( ',', $image_selectors );
$image_hover_selectors = implode( ',', $image_hover_selectors );
$this->start_controls_section(
'section_images',
[
'label' => __( 'Images', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_default_globals_notice();
$this->start_controls_tabs( 'tabs_image_style' );
$this->start_controls_tab(
'tab_image_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => 'image_border',
'selector' => $image_selectors,
'fields_options' => [
'color' => [
'dynamic' => [],
],
],
]
);
$this->add_responsive_control(
'image_border_radius',
[
'label' => __( 'Border Radius', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
$image_selectors => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->add_control(
'image_opacity',
[
'label' => __( 'Opacity', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'range' => [
'px' => [
'max' => 1,
'min' => 0.10,
'step' => 0.01,
],
],
'selectors' => [
$image_selectors => 'opacity: {{SIZE}};',
],
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => 'image_box_shadow',
'exclude' => [
'box_shadow_position',
],
'selector' => $image_selectors,
]
);
$this->add_group_control(
Group_Control_Css_Filter::get_type(),
[
'name' => 'image_css_filters',
'selector' => '{{WRAPPER}} img',
]
);
$this->end_controls_tab();
$this->start_controls_tab(
'tab_image_hover',
[
'label' => __( 'Hover', 'elementor' ),
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => 'image_hover_border',
'selector' => '{{WRAPPER}} img:hover',
'fields_options' => [
'color' => [
'dynamic' => [],
],
],
]
);
$this->add_responsive_control(
'image_hover_border_radius',
[
'label' => __( 'Border Radius', 'elementor' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', '%' ],
'selectors' => [
$image_hover_selectors => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->add_control(
'image_hover_opacity',
[
'label' => __( 'Opacity', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'range' => [
'px' => [
'max' => 1,
'min' => 0.10,
'step' => 0.01,
],
],
'selectors' => [
$image_hover_selectors => 'opacity: {{SIZE}};',
],
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => 'image_hover_box_shadow',
'exclude' => [
'box_shadow_position',
],
'selector' => $image_hover_selectors,
]
);
$this->add_group_control(
Group_Control_Css_Filter::get_type(),
[
'name' => 'image_hover_css_filters',
'selector' => $image_hover_selectors,
]
);
$this->add_control(
'image_hover_transition',
[
'label' => __( 'Transition Duration', 'elementor' ) . ' (s)',
'type' => Controls_Manager::SLIDER,
'range' => [
'px' => [
'max' => 3,
'step' => 0.1,
],
],
'selectors' => [
$image_selectors => 'transition-duration: {{SIZE}}s',
],
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
$this->end_controls_section();
}
}
@@ -0,0 +1,215 @@
<?php
namespace Elementor\Core\Kits\Documents\Tabs;
use Elementor\Controls_Manager;
use Elementor\Group_Control_Typography;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Theme_Style_Typography extends Tab_Base {
public function get_id() {
return 'theme-style-typography';
}
public function get_title() {
return __( 'Typography', 'elementor' );
}
public function register_tab_controls() {
$this->start_controls_section(
'section_typography',
[
'label' => __( 'Typography', 'elementor' ),
'tab' => $this->get_id(),
]
);
$this->add_default_globals_notice();
$this->add_control(
'body_heading',
[
'type' => Controls_Manager::HEADING,
'label' => __( 'Body', 'elementor' ),
]
);
$this->add_control(
'body_color',
[
'label' => __( 'Text Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
'{{WRAPPER}}' => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'body_typography',
'selector' => '{{WRAPPER}}',
]
);
$this->add_responsive_control(
'paragraph_spacing',
[
'label' => __( 'Paragraph Spacing', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'selectors' => [
'{{WRAPPER}} p' => 'margin-bottom: {{SIZE}}{{UNIT}}',
],
'range' => [
'px' => [
'min' => 0,
'max' => 100,
],
'em' => [
'min' => 0.1,
'max' => 20,
],
'vh' => [
'min' => 0,
'max' => 100,
],
],
'size_units' => [ 'px', 'em', 'vh' ],
]
);
//Link Selectors
$link_selectors = [
'{{WRAPPER}} a',
];
$link_hover_selectors = [
'{{WRAPPER}} a:hover',
];
$link_selectors = implode( ',', $link_selectors );
$link_hover_selectors = implode( ',', $link_hover_selectors );
$this->add_control(
'link_heading',
[
'type' => Controls_Manager::HEADING,
'label' => __( 'Link', 'elementor' ),
'separator' => 'before',
]
);
$this->start_controls_tabs( 'tabs_link_style' );
$this->start_controls_tab(
'tab_link_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_control(
'link_normal_color',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$link_selectors => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'link_normal_typography',
'selector' => $link_selectors,
]
);
$this->end_controls_tab();
$this->start_controls_tab(
'tab_link_hover',
[
'label' => __( 'Hover', 'elementor' ),
]
);
$this->add_control(
'link_hover_color',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$link_hover_selectors => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => 'link_hover_typography',
'selector' => $link_hover_selectors,
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
// Headings.
$this->add_element_controls( __( 'H1', 'elementor' ), 'h1', '{{WRAPPER}} h1' );
$this->add_element_controls( __( 'H2', 'elementor' ), 'h2', '{{WRAPPER}} h2' );
$this->add_element_controls( __( 'H3', 'elementor' ), 'h3', '{{WRAPPER}} h3' );
$this->add_element_controls( __( 'H4', 'elementor' ), 'h4', '{{WRAPPER}} h4' );
$this->add_element_controls( __( 'H5', 'elementor' ), 'h5', '{{WRAPPER}} h5' );
$this->add_element_controls( __( 'H6', 'elementor' ), 'h6', '{{WRAPPER}} h6' );
$this->end_controls_section();
}
private function add_element_controls( $label, $prefix, $selector ) {
$this->add_control(
$prefix . '_heading',
[
'type' => Controls_Manager::HEADING,
'label' => $label,
'separator' => 'before',
]
);
$this->add_control(
$prefix . '_color',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'dynamic' => [],
'selectors' => [
$selector => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'label' => __( 'Typography', 'elementor' ),
'name' => $prefix . '_typography',
'selector' => $selector,
]
);
}
}
@@ -0,0 +1,285 @@
<?php
namespace Elementor\Core\Kits;
use Elementor\Core\Kits\Controls\Repeater;
use Elementor\Core\Kits\Documents\Tabs\Global_Colors;
use Elementor\Core\Kits\Documents\Tabs\Global_Typography;
use Elementor\Plugin;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Core\Files\CSS\Post_Preview as Post_Preview;
use Elementor\Core\Documents_Manager;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Manager {
const OPTION_ACTIVE = 'elementor_active_kit';
public function get_active_id() {
$id = get_option( self::OPTION_ACTIVE );
$kit_document = Plugin::$instance->documents->get( $id );
if ( ! $kit_document || ! $kit_document instanceof Kit || 'trash' === $kit_document->get_main_post()->post_status ) {
$id = $this->create_default();
update_option( self::OPTION_ACTIVE, $id );
}
return $id;
}
public function get_active_kit() {
$id = $this->get_active_id();
return Plugin::$instance->documents->get( $id );
}
public function get_active_kit_for_frontend() {
$id = $this->get_active_id();
return Plugin::$instance->documents->get_doc_for_frontend( $id );
}
/**
* Init kit controls.
*
* A temp solution in order to avoid init kit group control from within another group control.
*
* After moving the `default_font` to the kit, the Typography group control cause initialize the kit controls at: https://github.com/elementor/elementor/blob/e6e1db9eddef7e3c1a5b2ba0c2338e2af2a3bfe3/includes/controls/groups/typography.php#L91
* and because the group control is a singleton, its args are changed to the last kit group control.
*/
public function init_kit_controls() {
$this->get_active_kit_for_frontend()->get_settings();
}
public function get_current_settings( $setting = null ) {
$kit = $this->get_active_kit_for_frontend();
if ( ! $kit ) {
return '';
}
return $kit->get_settings( $setting );
}
private function create_default() {
$kit = Plugin::$instance->documents->create( 'kit', [
'post_type' => Source_Local::CPT,
'post_title' => __( 'Default Kit', 'elementor' ),
'post_status' => 'publish',
] );
return $kit->get_id();
}
/**
* @param Documents_Manager $documents_manager
*/
public function register_document( $documents_manager ) {
$documents_manager->register_document_type( 'kit', Kit::get_class_full_name() );
}
public function localize_settings( $settings ) {
$kit = $this->get_active_kit();
$kit_controls = $kit->get_controls();
$design_system_controls = [
'colors' => $kit_controls['system_colors']['fields'],
'typography' => $kit_controls['system_typography']['fields'],
];
$settings = array_replace_recursive( $settings, [
'kit_id' => $kit->get_main_id(),
'kit_config' => [
'typography_prefix' => Global_Typography::TYPOGRAPHY_GROUP_PREFIX,
'design_system_controls' => $design_system_controls,
],
'user' => [
'can_edit_kit' => $kit->is_editable_by_current_user(),
],
'i18n' => [
'close' => __( 'Close', 'elementor' ),
'back' => __( 'Back', 'elementor' ),
'site_identity' => __( 'Site Identity', 'elementor' ),
'lightbox' => __( 'Lightbox', 'elementor' ),
'layout' => __( 'Layout', 'elementor' ),
'theme_style' => __( 'Theme Style', 'elementor' ),
'add_color' => __( 'Add Color', 'elementor' ),
'add_style' => __( 'Add Style', 'elementor' ),
'new_item' => __( 'New Item', 'elementor' ),
'global_color' => __( 'Global Color', 'elementor' ),
'global_fonts' => __( 'Global Fonts', 'elementor' ),
'global_colors' => __( 'Global Colors', 'elementor' ),
'invalid' => __( 'Invalid', 'elementor' ),
'color_cannot_be_deleted' => __( 'System Color can\'t be deleted', 'elementor' ),
'font_cannot_be_deleted' => __( 'System Font can\'t be deleted', 'elementor' ),
'design_system' => __( 'Design System', 'elementor' ),
'buttons' => __( 'Buttons', 'elementor' ),
'images' => __( 'Images', 'elementor' ),
'form_fields' => __( 'Form Fields', 'elementor' ),
'background' => __( 'Background', 'elementor' ),
'custom_css' => __( 'Custom CSS', 'elementor' ),
'additional_settings' => __( 'Additional Settings', 'elementor' ),
'kit_changes_updated' => __( 'Your changes have been updated.', 'elementor' ),
'back_to_editor' => __( 'Back to Editor', 'elementor' ),
],
] );
return $settings;
}
public function preview_enqueue_styles() {
$kit = $this->get_kit_for_frontend();
if ( $kit ) {
// On preview, the global style is not enqueued.
$this->frontend_before_enqueue_styles();
Plugin::$instance->frontend->print_fonts_links();
}
}
public function frontend_before_enqueue_styles() {
$kit = $this->get_kit_for_frontend();
if ( $kit ) {
if ( $kit->is_autosave() ) {
$css_file = Post_Preview::create( $kit->get_id() );
} else {
$css_file = Post_CSS::create( $kit->get_id() );
}
$css_file->enqueue();
}
}
public function render_panel_html() {
require __DIR__ . '/views/panel.php';
}
public function get_kit_for_frontend() {
$kit = false;
$active_kit = $this->get_active_kit();
$is_kit_preview = is_preview() && isset( $_GET['preview_id'] ) && $active_kit->get_main_id() === (int) $_GET['preview_id'];
if ( $is_kit_preview ) {
$kit = Plugin::$instance->documents->get_doc_or_auto_save( $active_kit->get_main_id(), get_current_user_id() );
} elseif ( 'publish' === $active_kit->get_main_post()->post_status ) {
$kit = $active_kit;
}
return $kit;
}
public function update_kit_settings_based_on_option( $key, $value ) {
/** @var Kit $active_kit */
$active_kit = $this->get_active_kit();
if ( $active_kit->is_saving() ) {
return;
}
$active_kit->update_settings( [ $key => $value ] );
}
/**
* Map Scheme To Global
*
* Convert a given scheme value to its corresponding default global value
*
* @param string $type 'color'/'typography'
* @param $value
* @return mixed
*/
private function map_scheme_to_global( $type, $value ) {
$schemes_to_globals_map = [
'color' => [
'1' => Global_Colors::COLOR_PRIMARY,
'2' => Global_Colors::COLOR_SECONDARY,
'3' => Global_Colors::COLOR_TEXT,
'4' => Global_Colors::COLOR_ACCENT,
],
'typography' => [
'1' => Global_Typography::TYPOGRAPHY_PRIMARY,
'2' => Global_Typography::TYPOGRAPHY_SECONDARY,
'3' => Global_Typography::TYPOGRAPHY_TEXT,
'4' => Global_Typography::TYPOGRAPHY_ACCENT,
],
];
return $schemes_to_globals_map[ $type ][ $value ];
}
/**
* Convert Scheme to Default Global
*
* If a control has a scheme property, convert it to a default Global.
*
* @param $scheme - Control scheme property
* @return array - Control/group control args
* @since 3.0.0
* @access public
*/
public function convert_scheme_to_global( $scheme ) {
if ( isset( $scheme['type'] ) && isset( $scheme['value'] ) ) {
//_deprecated_argument( $args['scheme'], '3.0.0', 'Schemes are now deprecated - use $args[\'global\'] instead.' );
return $this->map_scheme_to_global( $scheme['type'], $scheme['value'] );
}
// Typography control 'scheme' properties usually only include the string with the typography value ('1'-'4').
return $this->map_scheme_to_global( 'typography', $scheme );
}
public function register_controls() {
$controls_manager = Plugin::$instance->controls_manager;
$controls_manager->register_control( Repeater::CONTROL_TYPE, new Repeater() );
}
public function is_custom_colors_enabled() {
return ! get_option( 'elementor_disable_color_schemes' );
}
public function is_custom_typography_enabled() {
return ! get_option( 'elementor_disable_typography_schemes' );
}
/**
* Add kit wrapper body class.
*
* It should be added even for non Elementor pages,
* in order to support embedded templates.
*/
private function add_body_class() {
$kit = $this->get_kit_for_frontend();
if ( $kit ) {
Plugin::$instance->frontend->add_body_class( 'elementor-kit-' . $kit->get_main_id() );
}
}
public function __construct() {
add_action( 'elementor/documents/register', [ $this, 'register_document' ] );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_filter( 'elementor/editor/footer', [ $this, 'render_panel_html' ] );
add_action( 'elementor/frontend/after_enqueue_styles', [ $this, 'frontend_before_enqueue_styles' ], 0 );
add_action( 'elementor/preview/enqueue_styles', [ $this, 'preview_enqueue_styles' ], 0 );
add_action( 'elementor/controls/controls_registered', [ $this, 'register_controls' ] );
add_action( 'update_option_blogname', function ( $old_value, $value ) {
$this->update_kit_settings_based_on_option( 'site_name', $value );
}, 10, 2 );
add_action( 'update_option_blogdescription', function ( $old_value, $value ) {
$this->update_kit_settings_based_on_option( 'site_description', $value );
}, 10, 2 );
add_action( 'wp_head', function() {
$this->add_body_class();
} );
}
}
@@ -0,0 +1,36 @@
<script type="text/template" id="tmpl-elementor-kit-panel">
<main id="elementor-kit__panel-content__wrapper" class="elementor-panel-content-wrapper"></main>
</script>
<script type="text/template" id="tmpl-elementor-kit-panel-content">
<div id="elementor-kit-panel-content-controls"></div>
<#
const tabConfig = $e.components.get( 'panel/global' ).getActiveTabConfig();
if ( tabConfig.helpUrl ) { #>
<div id="elementor-panel__editor__help">
<a id="elementor-panel__editor__help__link" href="{{ tabConfig.helpUrl }}" target="_blank">
<?php echo __( 'Need Help', 'elementor' ); ?>
<i class="eicon-help-o"></i>
</a>
</div>
<# } #>
</script>
<script type="text/template" id="tmpl-elementor-global-style-repeater-row">
<# let removeClass = 'remove',
removeIcon = 'eicon-trash-o';
if ( ! itemActions.remove ) {
removeClass += '--disabled';
removeIcon = 'eicon-disable-trash-o'
}
#>
<div class="elementor-repeater-row-tool elementor-repeater-tool-{{{ removeClass }}}">
<i class="{{{ removeIcon }}}" aria-hidden="true"></i>
<# if ( itemActions.remove ) { #>
<span class="elementor-screen-only"><?php echo __( 'Remove', 'elementor' ); ?></span>
<# } #>
</div>
<div class="elementor-repeater-row-controls"></div>
</script>
@@ -0,0 +1,177 @@
<?php
namespace Elementor\Core\Logger\Items;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Base implements Log_Item_Interface {
const FORMAT = 'date [type] message [meta]';
const TRACE_FORMAT = '#key: file(line): class type function()';
const TRACE_LIMIT = 5;
protected $date;
protected $type;
protected $message;
protected $meta = [];
protected $times = 0;
protected $times_dates = [];
protected $args = [];
public function __construct( $args ) {
$this->date = current_time( 'mysql' );
$this->message = ! empty( $args['message'] ) ? esc_html( $args['message'] ) : '';
$this->type = ! empty( $args['type'] ) ? $args['type'] : 'info';
$this->meta = ! empty( $args['meta'] ) ? $args['meta'] : [];
$this->args = $args;
$this->set_trace();
}
public function __get( $name ) {
if ( property_exists( $this, $name ) ) {
return $this->{$name};
}
return '';
}
public function __toString() {
$vars = get_object_vars( $this );
return strtr( static::FORMAT, $vars );
}
public function jsonSerialize() {
return [
'class' => get_class( $this ),
'item' => [
'date' => $this->date,
'message' => $this->message,
'type' => $this->type,
'meta' => $this->meta,
'times' => $this->times,
'times_dates' => $this->times_dates,
'args' => $this->args,
],
];
}
public function deserialize( $properties ) {
$this->date = ! empty( $properties['date'] ) && is_string( $properties['date'] ) ? $properties['date'] : '';
$this->message = ! empty( $properties['message'] ) && is_string( $properties['message'] ) ? $properties['message'] : '';
$this->type = ! empty( $properties['type'] ) && is_string( $properties['type'] ) ? $properties['type'] : '';
$this->meta = ! empty( $properties['meta'] ) && is_array( $properties['meta'] ) ? $properties['meta'] : [];
$this->times = ! empty( $properties['times'] ) && is_string( $properties['times'] ) ? $properties['times'] : '';
$this->times_dates = ! empty( $properties['times_dates'] ) && is_array( $properties['times_dates'] ) ? $properties['times_dates'] : [];
$this->args = ! empty( $properties['args'] ) && is_array( $properties['args'] ) ? $properties['args'] : [];
}
/**
* @return Log_Item_Interface | null
*/
public static function from_json( $str ) {
$obj = json_decode( $str, true );
if ( ! array_key_exists( 'class', $obj ) ) {
return null;
}
$class = $obj['class'];
if ( class_exists( $class ) ) {
/** @var Base $item */
$item = new $class( $obj['item']['message'] );
$item->deserialize( $obj['item'] );
return $item;
}
return null;
}
public function to_formatted_string( $output_format = 'html' ) {
$vars = get_object_vars( $this );
$format = static::FORMAT;
if ( 'html' === $output_format ) {
$format = str_replace( 'message', '<strong>message</strong>', static::FORMAT );
}
if ( empty( $vars['meta'] ) ) {
$format = str_replace( '[meta]', '', $format );
} else {
$vars['meta'] = stripslashes( var_export( $vars['meta'], true ) ); // @codingStandardsIgnoreLine
}
return strtr( $format, $vars );
}
public function get_fingerprint() {
$unique_key = $this->type . $this->message . var_export( $this->meta, true ); // @codingStandardsIgnoreLine
//info messages are not be aggregated:
if ( 'info' === $this->type ) {
$unique_key .= $this->date;
}
return md5( $unique_key );
}
public function increase_times( $item, $truncate = true ) {
$this->times++;
$this->times_dates[] = $item->date;
if ( $truncate && ( self::MAX_LOG_ENTRIES < count( $this->times_dates ) ) ) {
$this->times_dates = array_slice( $this->times_dates, -self::MAX_LOG_ENTRIES );
}
}
public function format( $format = 'html' ) {
$trace = $this->format_trace();
if ( empty( $trace ) ) {
return $this->to_formatted_string( $format );
}
$copy = clone $this;
$copy->meta['trace'] = $trace;
return $copy->to_formatted_string( $format );
}
public function get_name() {
return 'Log';
}
private function format_trace() {
$trace = empty( $this->meta['trace'] ) ? '' : $this->meta['trace'];
if ( is_string( $trace ) ) {
return $trace;
}
$trace_str = '';
foreach ( $trace as $key => $trace_line ) {
$format = static::TRACE_FORMAT;
$trace_line['key'] = $key;
if ( empty( $trace_line['file'] ) ) {
$format = str_replace( 'file(line): ', '', $format );
}
$trace_str .= PHP_EOL . strtr( $format, $trace_line );
$trace_str .= empty( $trace_line['args'] ) ? '' : var_export( $trace_line['args'], true ); // @codingStandardsIgnoreLine
}
return $trace_str . PHP_EOL;
}
private function set_trace() {
if ( ! empty( $this->args['trace'] ) && true === $this->args['trace'] ) {
$limit = empty( $this->args['trace_limit'] ) ? static::TRACE_LIMIT : $this->args['trace_limit'];
$stack = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // @codingStandardsIgnoreLine
while ( ! empty( $stack ) && ! empty( $stack[0]['file'] ) && ( false !== strpos( $stack[0]['file'], 'core' . DIRECTORY_SEPARATOR . 'logger' ) ) ) {
array_shift( $stack );
}
$this->meta['trace'] = array_slice( $stack, 0, $limit );
return;
}
if ( is_array( $this->args ) ) {
unset( $this->args['trace'] );
}
}
}
@@ -0,0 +1,38 @@
<?php
namespace Elementor\Core\Logger\Items;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class File extends Base {
const FORMAT = 'date [type X times][file:line] message [meta]';
protected $file;
protected $line;
public function __construct( $args ) {
parent::__construct( $args );
$this->file = empty( $args['file'] ) ? '' : $args['file'];
$this->line = empty( $args['line'] ) ? '' : $args['line'];
}
public function jsonSerialize() {
$json_arr = parent::jsonSerialize();
$json_arr['file'] = $this->file;
$json_arr['line'] = $this->line;
return $json_arr;
}
public function deserialize( $properties ) {
parent::deserialize( $properties );
$this->file = ! empty( $properties['file'] ) && is_string( $properties['file'] ) ? $properties['file'] : '';
$this->line = ! empty( $properties['line'] ) && is_string( $properties['line'] ) ? $properties['line'] : '';
}
public function get_name() {
return 'File';
}
}
@@ -0,0 +1,35 @@
<?php
namespace Elementor\Core\Logger\Items;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class JS extends File {
const FORMAT = 'JS: date [type X times][file:line:column] message [meta]';
protected $column;
public function __construct( $args ) {
parent::__construct( $args );
$this->column = $args['column'];
$this->file = $args['url'];
$this->date = gmdate( 'Y-m-d H:i:s', $args['timestamp'] );
}
public function jsonSerialize() {
$json_arr = parent::jsonSerialize();
$json_arr['column'] = $this->column;
return $json_arr;
}
public function deserialize( $properties ) {
parent::deserialize( $properties );
$this->column = ! empty( $properties['column'] ) && is_string( $properties['column'] ) ? $properties['column'] : '';
}
public function get_name() {
return 'JS';
}
}
@@ -0,0 +1,73 @@
<?php
namespace Elementor\Core\Logger\Items;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Interface Log_Item_Interface
*
* @package Elementor\Core\Logger
*
* @property string $date
* @property string $type
* @property string $message
* @property int $times
* @property array $meta
* @property array $times_dates
* @property array $args
*
*/
interface Log_Item_Interface extends \JsonSerializable {
const MAX_LOG_ENTRIES = 42;
/**
* Log_Item_Interface constructor.
*
* @param array $args
*/
public function __construct( $args );
/**
* @param string $name
*
* @return string
*/
public function __get( $name );
/**
* @return string
*/
public function __toString();
/**
* @param $str
* @return Log_Item_Interface | null
*/
public static function from_json( $str );
/**
* @param string $format
* @return string
*/
public function format( $format = 'html' );
/**
* @return string
*/
public function get_fingerprint();
/**
* @param Log_Item_Interface $item
* @param bool $truncate
*/
public function increase_times( $item, $truncate = true );
/**
* @return string
*/
public function get_name();
}
@@ -0,0 +1,15 @@
<?php
namespace Elementor\Core\Logger\Items;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class PHP extends File {
const FORMAT = 'PHP: date [type X times][file::line] message [meta]';
public function get_name() {
return 'PHP';
}
}
@@ -0,0 +1,110 @@
<?php
namespace Elementor\Core\Logger;
use Elementor\Modules\System_Info\Reporters\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Log reporter.
*
* Elementor log reporter handler class is responsible for generating the
* debug reports.
*
* @since 2.4.0
*/
class Log_Reporter extends Base {
const MAX_ENTRIES = 20;
const CLEAR_LOG_ACTION = 'elementor-clear-log';
public function get_title() {
$title = 'Log';
if ( 'html' === $this->_properties['format'] && empty( $_GET[ self::CLEAR_LOG_ACTION ] ) ) { // phpcs:ignore -- nonce validation is not require here.
$nonce = wp_create_nonce( self::CLEAR_LOG_ACTION );
$url = add_query_arg( [
self::CLEAR_LOG_ACTION => 1,
'_wpnonce' => $nonce,
] );
$title .= '<a href="' . esc_url( $url ) . '#elementor-clear-log" class="box-title-tool">' . __( 'Clear Log', 'elementor' ) . '</a>';
$title .= '<span id="elementor-clear-log"></span>';
}
return $title;
}
public function get_fields() {
return [
'log_entries' => '',
];
}
public function get_log_entries() {
/** @var \Elementor\Core\Logger\Manager $manager */
$manager = Manager::instance();
/** @var \Elementor\Core\Logger\Loggers\Db $logger */
$logger = $manager->get_logger( 'db' );
if ( ! empty( $_GET[ self::CLEAR_LOG_ACTION ] ) ) {
if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], self::CLEAR_LOG_ACTION ) ) {
wp_die( 'Invalid Nonce', 'Invalid Nonce', [
'back_link' => true,
] );
}
$logger->clear();
}
$log_string = 'No entries to display';
$log_entries = $logger->get_formatted_log_entries( self::MAX_ENTRIES, false );
if ( ! empty( $log_entries ) ) {
$entries_string = '';
foreach ( $log_entries as $key => $log_entry ) {
if ( $log_entry['count'] ) {
$entries_string .= '<h3>' . sprintf( '%s: showing %s of %s', $key, $log_entry['count'], $log_entry['total_count'] ) . '</h3>';
$entries_string .= '<div class="elementor-log-entries">' . $log_entry['entries'] . '</div>';
}
}
if ( ! empty( $entries_string ) ) {
$log_string = $entries_string;
}
}
return [
'value' => $log_string,
];
}
public function get_raw_log_entries() {
$log_string = 'No entries to display';
/** @var \Elementor\Core\Logger\Manager $manager */
$manager = Manager::instance();
$logger = $manager->get_logger();
$log_entries = $logger->get_formatted_log_entries( self::MAX_ENTRIES, false );
if ( ! empty( $log_entries ) ) {
$entries_string = PHP_EOL;
foreach ( $log_entries as $key => $log_entry ) {
if ( $log_entry['count'] ) {
$entries_string .= sprintf( '%s: showing %s of %s', $key, $log_entry['count'], $log_entry['total_count'] ) . $log_entry['entries'] . PHP_EOL;
}
}
if ( ! empty( $entries_string ) ) {
$log_string = $entries_string;
}
}
return [
'value' => $log_string,
];
}
}
@@ -0,0 +1,93 @@
<?php
namespace Elementor\Core\Logger\Loggers;
use Elementor\Core\Logger\Items\Base as Log_Item;
use Elementor\Core\Logger\Items\Log_Item_Interface as Log_Item_Interface;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Base implements Logger_Interface {
abstract protected function save_log( Log_Item_Interface $item );
/**
* @return Log_Item_Interface[]
*/
abstract public function get_log();
public function log( $item, $type = self::LEVEL_INFO, $args = [] ) {
if ( ! $item instanceof Log_Item ) {
$item = $this->create_item( $item, $type, $args );
}
$this->save_log( $item );
}
public function info( $message, $args = [] ) {
$this->log( $message, self::LEVEL_INFO, $args );
}
public function notice( $message, $args = [] ) {
$this->log( $message, self::LEVEL_NOTICE, $args );
}
public function warning( $message, $args = [] ) {
$this->log( $message, self::LEVEL_WARNING, $args );
}
public function error( $message, $args = [] ) {
$this->log( $message, self::LEVEL_ERROR, $args );
}
/**
* @param string $message
* @param string $type
* @param array $args
*
* @return Log_Item_Interface
*/
private function create_item( $message, $type, $args = [] ) {
$args['message'] = $message;
$args['type'] = $type;
$item = new Log_Item( $args );
return $item;
}
public function get_formatted_log_entries( $max_entries, $table = true ) {
$entries = $this->get_log();
if ( empty( $entries ) ) {
return [
'All' => [
'total_count' => 0,
'count' => 0,
'entries' => '',
],
];
}
$sorted_entries = [];
$open_tag = $table ? '<tr><td>' : '';
$close_tab = $table ? '</td></tr>' : PHP_EOL;
$format = $table ? 'html' : 'raw';
foreach ( $entries as $entry ) {
/** @var Log_Item $entry */
$sorted_entries[ $entry->get_name() ][] = $open_tag . $entry->format( $format ) . $close_tab;
}
$formatted_entries = [];
foreach ( $sorted_entries as $key => $sorted_entry ) {
$formatted_entries[ $key ]['total_count'] = count( $sorted_entry );
$formatted_entries[ $key ]['count'] = count( $sorted_entry );
$sorted_entry = array_slice( $sorted_entry, -$max_entries );
$formatted_entries[ $key ]['count'] = count( $sorted_entry );
$formatted_entries[ $key ]['entries'] = implode( $sorted_entry );
}
return $formatted_entries;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Elementor\Core\Logger\Loggers;
use Elementor\Core\Logger\Items\Log_Item_Interface as Log_Item;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Db extends Base {
public function save_log( Log_Item $item ) {
$log = $this->maybe_truncate_log();
$id = $item->get_fingerprint();
if ( empty( $log[ $id ] ) ) {
$log[ $id ] = $item;
}
$log[ $id ]->increase_times( $item );
update_option( self::LOG_NAME, $log, 'no' );
}
public function clear() {
delete_option( self::LOG_NAME );
}
private function maybe_truncate_log() {
/** @var Log_Item[] $log */
$log = $this->get_log();
if ( Log_Item::MAX_LOG_ENTRIES < count( $log ) ) {
$log = array_slice( $log, -Log_Item::MAX_LOG_ENTRIES );
}
return $log;
}
public function get_log() {
// Clear cache.
wp_cache_delete( self::LOG_NAME, 'options' );
$log = get_option( self::LOG_NAME, [] );
// In case the DB log is corrupted.
if ( ! is_array( $log ) ) {
$log = [];
}
return $log;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Elementor\Core\Logger\Loggers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
interface Logger_Interface {
const LEVEL_INFO = 'info';
const LEVEL_NOTICE = 'notice';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';
const LOG_NAME = 'elementor_log';
/**
* @param string $message
* @param string $type
* @param array $meta
*
* @return void
*/
public function log( $message, $type = self::LEVEL_INFO, $meta = [] );
/**
* @param string $message
* @param array $meta
*
* @return void
*/
public function info( $message, $meta = [] );
/**
* @param string $message
* @param array $meta
*
* @return void
*/
public function notice( $message, $meta = [] );
/**
* @param string $message
* @param array $meta
*
* @return void
*/
public function warning( $message, $meta = [] );
/**
* @param string $message
* @param array $meta
*
* @return void
*/
public function error( $message, $meta = [] );
/**
* @param int $max_entries
* @param bool $table use <td> in format
*
* @return array [ 'key' => [ 'total_count' => int, 'count' => int, 'entries' => Log_Item[] ] ]
*/
public function get_formatted_log_entries( $max_entries, $table = true );
}
@@ -0,0 +1,249 @@
<?php
namespace Elementor\Core\Logger;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module;
use Elementor\Core\Logger\Loggers\Logger_Interface;
use Elementor\Core\Logger\Items\PHP;
use Elementor\Core\Logger\Items\JS;
use Elementor\Plugin;
use Elementor\Modules\System_Info\Module as System_Info;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Manager extends BaseModule {
protected $loggers = [];
protected $default_logger = '';
public function get_name() {
return 'log';
}
public function shutdown( $last_error = null ) {
if ( ! $last_error ) {
$last_error = error_get_last();
}
if ( ! $last_error ) {
return;
}
if ( empty( $last_error['file'] ) ) {
return;
}
$error_path = ( wp_normalize_path( $last_error['file'] ) );
// `untrailingslashit` in order to include other plugins prefixed with elementor.
$elementor_path = untrailingslashit( wp_normalize_path( ELEMENTOR_PATH ) );
if ( false === strpos( $error_path, $elementor_path ) ) {
return;
}
$last_error['type'] = $this->get_log_type_from_php_error( $last_error['type'] );
$last_error['trace'] = true;
$item = new PHP( $last_error );
$this->get_logger()->log( $item );
}
public function rest_error_handler( $error_number, $error_message, $error_file, $error_line ) {
$error = new \WP_Error( $error_number, $error_message, [
'type' => $error_number,
'message' => $error_message,
'file' => $error_file,
'line' => $error_line,
] );
// Notify $e.data.
if ( ! headers_sent() ) {
header( 'Content-Type: application/json; charset=UTF-8' );
http_response_code( 500 );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
echo wp_json_encode( $error->get_error_data() );
} else {
echo wp_json_encode( [
'message' => 'Server error, see Elementor => System Info',
] );
}
}
$this->shutdown( $error->get_error_data() );
exit;
}
public function add_system_info_report() {
System_Info::add_report(
'log', [
'file_name' => __DIR__ . '/log-reporter.php',
'class_name' => __NAMESPACE__ . '\Log_Reporter',
]
);
}
/**
* Javascript log.
*
* Log Elementor errors and save them in the database.
*
* Fired by `wp_ajax_elementor_js_log` action.
*
*/
public function js_log() {
/** @var Module $ajax */
$ajax = Plugin::$instance->common->get_component( 'ajax' );
if ( ! $ajax->verify_request_nonce() || empty( $_POST['data'] ) ) {
wp_send_json_error();
}
array_walk_recursive( $_POST['data'], function( &$value ) {
$value = sanitize_text_field( $value );
} );
foreach ( $_POST['data'] as $error ) {
$error['type'] = Logger_Interface::LEVEL_ERROR;
if ( ! empty( $error['customFields'] ) ) {
$error['meta'] = $error['customFields'];
}
$item = new JS( $error );
$this->get_logger()->log( $item );
}
wp_send_json_success();
}
public function register_logger( $name, $class ) {
$this->loggers[ $name ] = $class;
}
public function set_default_logger( $name ) {
if ( ! empty( $this->loggers[ $name ] ) ) {
$this->default_logger = $name;
}
}
public function register_default_loggers() {
$this->register_logger( 'db', 'Elementor\Core\Logger\Loggers\Db' );
$this->set_default_logger( 'db' );
}
/**
* @param string $name
*
* @return Logger_Interface
*/
public function get_logger( $name = '' ) {
$this->register_loggers();
if ( empty( $name ) || ! isset( $this->loggers[ $name ] ) ) {
$name = $this->default_logger;
}
if ( ! $this->get_component( $name ) ) {
$this->add_component( $name, new $this->loggers[ $name ]() );
}
return $this->get_component( $name );
}
/**
* @param string $message
* @param array $args
*
* @return void
*/
public function log( $message, $args = [] ) {
$this->get_logger()->log( $message, $args );
}
/**
* @param string $message
* @param array $args
*
* @return void
*/
public function info( $message, $args = [] ) {
$this->get_logger()->info( $message, $args );
}
/**
* @param string $message
* @param array $args
*
* @return void
*/
public function notice( $message, $args = [] ) {
$this->get_logger()->notice( $message, $args );
}
/**
* @param string $message
* @param array $args
*
* @return void
*/
public function warning( $message, $args = [] ) {
$this->get_logger()->warning( $message, $args );
}
/**
* @param string $message
* @param array $args
*
* @return void
*/
public function error( $message, $args = [] ) {
$this->get_logger()->error( $message, $args );
}
private function get_log_type_from_php_error( $type ) {
$error_map = [
E_CORE_ERROR => Logger_Interface::LEVEL_ERROR,
E_ERROR => Logger_Interface::LEVEL_ERROR,
E_USER_ERROR => Logger_Interface::LEVEL_ERROR,
E_COMPILE_ERROR => Logger_Interface::LEVEL_ERROR,
E_RECOVERABLE_ERROR => Logger_Interface::LEVEL_ERROR,
E_PARSE => Logger_Interface::LEVEL_ERROR,
E_STRICT => Logger_Interface::LEVEL_ERROR,
E_WARNING => Logger_Interface::LEVEL_WARNING,
E_USER_WARNING => Logger_Interface::LEVEL_WARNING,
E_CORE_WARNING => Logger_Interface::LEVEL_WARNING,
E_COMPILE_WARNING => Logger_Interface::LEVEL_WARNING,
E_NOTICE => Logger_Interface::LEVEL_NOTICE,
E_USER_NOTICE => Logger_Interface::LEVEL_NOTICE,
E_DEPRECATED => Logger_Interface::LEVEL_NOTICE,
E_USER_DEPRECATED => Logger_Interface::LEVEL_NOTICE,
];
return isset( $error_map[ $type ] ) ? $error_map[ $type ] : Logger_Interface::LEVEL_ERROR;
}
private function register_loggers() {
if ( ! did_action( 'elementor/loggers/register' ) ) {
do_action( 'elementor/loggers/register', $this );
}
}
public function __construct() {
register_shutdown_function( [ $this, 'shutdown' ] );
add_action( 'admin_init', [ $this, 'add_system_info_report' ], 80 );
add_action( 'wp_ajax_elementor_js_log', [ $this, 'js_log' ] );
add_action( 'elementor/loggers/register', [ $this, 'register_default_loggers' ] );
}
}
@@ -0,0 +1,118 @@
<?php
namespace Elementor\Core;
use Elementor\Core\Base\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor modules manager.
*
* Elementor modules manager handler class is responsible for registering and
* managing Elementor modules.
*
* @since 1.6.0
*/
class Modules_Manager {
/**
* Registered modules.
*
* Holds the list of all the registered modules.
*
* @since 1.6.0
* @access public
*
* @var array
*/
private $modules = [];
/**
* Modules manager constructor.
*
* Initializing the Elementor modules manager.
*
* @since 1.6.0
* @access public
*/
public function __construct() {
$modules_namespace_prefix = $this->get_modules_namespace_prefix();
foreach ( $this->get_modules_names() as $module_name ) {
$class_name = str_replace( '-', ' ', $module_name );
$class_name = str_replace( ' ', '', ucwords( $class_name ) );
$class_name = $modules_namespace_prefix . '\\Modules\\' . $class_name . '\Module';
/** @var Module $class_name */
if ( $class_name::is_active() ) {
$this->modules[ $module_name ] = $class_name::instance();
}
}
}
/**
* Get modules names.
*
* Retrieve the modules names.
*
* @since 2.0.0
* @access public
*
* @return string[] Modules names.
*/
public function get_modules_names() {
return [
'admin-bar',
'history',
'library',
'dynamic-tags',
'page-templates',
'gutenberg',
'wp-cli',
'safe-mode',
'usage',
];
}
/**
* Get modules.
*
* Retrieve all the registered modules or a specific module.
*
* @since 2.0.0
* @access public
*
* @param string $module_name Module name.
*
* @return null|Module|Module[] All the registered modules or a specific module.
*/
public function get_modules( $module_name ) {
if ( $module_name ) {
if ( isset( $this->modules[ $module_name ] ) ) {
return $this->modules[ $module_name ];
}
return null;
}
return $this->modules;
}
/**
* Get modules namespace prefix.
*
* Retrieve the modules namespace prefix.
*
* @since 2.0.0
* @access protected
*
* @return string Modules namespace prefix.
*/
protected function get_modules_namespace_prefix() {
return 'Elementor';
}
}
@@ -0,0 +1,143 @@
<?php
namespace Elementor\Core\Responsive\Files;
use Elementor\Core\Files\Base;
use Elementor\Core\Responsive\Responsive;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Frontend extends Base {
const META_KEY = 'elementor-custom-breakpoints-files';
private $template_file;
/**
* @since 2.1.0
* @access public
*/
public function __construct( $file_name, $template_file = null ) {
$this->template_file = $template_file;
parent::__construct( $file_name );
}
/**
* @since 2.1.0
* @access public
*/
public function parse_content() {
$breakpoints = Responsive::get_breakpoints();
$breakpoints_keys = array_keys( $breakpoints );
$file_content = file_get_contents( $this->template_file );
$file_content = preg_replace_callback( '/ELEMENTOR_SCREEN_([A-Z]+)_([A-Z]+)/', function ( $placeholder_data ) use ( $breakpoints_keys, $breakpoints ) {
$breakpoint_index = array_search( strtolower( $placeholder_data[1] ), $breakpoints_keys );
$is_max_point = 'MAX' === $placeholder_data[2];
if ( $is_max_point ) {
$breakpoint_index++;
}
$value = $breakpoints[ $breakpoints_keys[ $breakpoint_index ] ];
if ( $is_max_point ) {
$value--;
}
return $value . 'px';
}, $file_content );
return $file_content;
}
/**
* Load meta.
*
* Retrieve the file meta data.
*
* @since 2.1.0
* @access protected
*/
protected function load_meta() {
$option = $this->load_meta_option();
$file_meta_key = $this->get_file_meta_key();
if ( empty( $option[ $file_meta_key ] ) ) {
return [];
}
return $option[ $file_meta_key ];
}
/**
* Update meta.
*
* Update the file meta data.
*
* @since 2.1.0
* @access protected
*
* @param array $meta New meta data.
*/
protected function update_meta( $meta ) {
$option = $this->load_meta_option();
$option[ $this->get_file_meta_key() ] = $meta;
update_option( static::META_KEY, $option );
}
/**
* Delete meta.
*
* Delete the file meta data.
*
* @since 2.1.0
* @access protected
*/
protected function delete_meta() {
$option = $this->load_meta_option();
$file_meta_key = $this->get_file_meta_key();
if ( isset( $option[ $file_meta_key ] ) ) {
unset( $option[ $file_meta_key ] );
}
if ( $option ) {
update_option( static::META_KEY, $option );
} else {
delete_option( static::META_KEY );
}
}
/**
* @since 2.1.0
* @access private
*/
private function get_file_meta_key() {
return pathinfo( $this->get_file_name(), PATHINFO_FILENAME );
}
/**
* @since 2.1.0
* @access private
*/
private function load_meta_option() {
$option = get_option( static::META_KEY );
if ( ! $option ) {
$option = [];
}
return $option;
}
}
@@ -0,0 +1,168 @@
<?php
namespace Elementor\Core\Responsive;
use Elementor\Core\Responsive\Files\Frontend;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor responsive.
*
* Elementor responsive handler class is responsible for setting up Elementor
* responsive breakpoints.
*
* @since 1.0.0
*/
class Responsive {
/**
* The Elementor breakpoint prefix.
*/
const BREAKPOINT_OPTION_PREFIX = 'viewport_';
/**
* Default breakpoints.
*
* Holds the default responsive breakpoints.
*
* @since 1.0.0
* @access private
* @static
*
* @var array Default breakpoints.
*/
private static $default_breakpoints = [
'xs' => 0,
'sm' => 480,
'md' => 768,
'lg' => 1025,
'xl' => 1440,
'xxl' => 1600,
];
/**
* Editable breakpoint keys.
*
* Holds the editable breakpoint keys.
*
* @since 1.0.0
* @access private
* @static
*
* @var array Editable breakpoint keys.
*/
private static $editable_breakpoints_keys = [
'md',
'lg',
];
/**
* Get default breakpoints.
*
* Retrieve the default responsive breakpoints.
*
* @since 1.0.0
* @access public
* @static
*
* @return array Default breakpoints.
*/
public static function get_default_breakpoints() {
return self::$default_breakpoints;
}
/**
* Get editable breakpoints.
*
* Retrieve the editable breakpoints.
*
* @since 1.0.0
* @access public
* @static
*
* @return array Editable breakpoints.
*/
public static function get_editable_breakpoints() {
return array_intersect_key( self::get_breakpoints(), array_flip( self::$editable_breakpoints_keys ) );
}
/**
* Get breakpoints.
*
* Retrieve the responsive breakpoints.
*
* @since 1.0.0
* @access public
* @static
*
* @return array Responsive breakpoints.
*/
public static function get_breakpoints() {
return array_reduce(
array_keys( self::$default_breakpoints ), function( $new_array, $breakpoint_key ) {
if ( ! in_array( $breakpoint_key, self::$editable_breakpoints_keys ) ) {
$new_array[ $breakpoint_key ] = self::$default_breakpoints[ $breakpoint_key ];
} else {
$saved_option = Plugin::$instance->kits_manager->get_current_settings( self::BREAKPOINT_OPTION_PREFIX . $breakpoint_key );
$new_array[ $breakpoint_key ] = $saved_option ? (int) $saved_option : self::$default_breakpoints[ $breakpoint_key ];
}
return $new_array;
}, []
);
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function has_custom_breakpoints() {
return ! ! array_diff( self::$default_breakpoints, self::get_breakpoints() );
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function get_stylesheet_templates_path() {
return ELEMENTOR_ASSETS_PATH . 'css/templates/';
}
/**
* @since 2.1.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();
}
}
/**
* @since 2.1.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;
}
return apply_filters( 'elementor/core/responsive/get_stylesheet_templates', $templates );
}
}
@@ -0,0 +1,246 @@
<?php
namespace Elementor\Core\RoleManager;
use Elementor\Settings_Page;
use Elementor\Settings;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Role_Manager extends Settings_Page {
const PAGE_ID = 'elementor-role-manager';
const ROLE_MANAGER_OPTION_NAME = 'exclude_user_roles';
/**
* @since 2.0.0
* @access public
*/
public function get_role_manager_options() {
return get_option( 'elementor_' . self::ROLE_MANAGER_OPTION_NAME, [] );
}
/**
* @since 2.0.0
* @access protected
*/
protected function get_page_title() {
return __( 'Role Manager', 'elementor' );
}
/**
* @since 2.0.0
* @access public
*/
public function register_admin_menu() {
add_submenu_page(
Settings::PAGE_ID,
$this->get_page_title(),
$this->get_page_title(),
'manage_options',
self::PAGE_ID,
[ $this, 'display_settings_page' ]
);
}
/**
* @since 2.0.0
* @access protected
*/
protected function create_tabs() {
$validation_class = 'Elementor\Settings_Validations';
return [
'general' => [
'label' => __( 'General', 'elementor' ),
'sections' => [
'tools' => [
'fields' => [
'exclude_user_roles' => [
'label' => __( 'Exclude Roles', 'elementor' ),
'field_args' => [
'type' => 'checkbox_list_roles',
'exclude' => [ 'super_admin', 'administrator' ],
],
'setting_args' => [
'sanitize_callback' => [ $validation_class, 'checkbox_list' ],
],
],
],
],
],
],
];
}
/**
* @since 2.0.0
* @access public
*/
public function display_settings_page() {
$this->get_tabs();
?>
<div class="wrap">
<h1><?php echo esc_html( $this->get_page_title() ); ?></h1>
<div id="elementor-role-manager">
<h3><?php echo __( 'Manage What Your Users Can Edit In Elementor', 'elementor' ); ?></h3>
<form id="elementor-settings-form" method="post" action="options.php">
<?php
settings_fields( static::PAGE_ID );
echo '<div class="elementor-settings-form-page elementor-active">';
foreach ( get_editable_roles() as $role_slug => $role_data ) {
if ( 'administrator' === $role_slug ) {
continue;
}
$this->display_role_controls( $role_slug, $role_data );
}
submit_button();
?>
</form>
</div>
</div><!-- /.wrap -->
<?php
}
/**
* @since 2.0.0
* @access private
*
* @param string $role_slug The role slug.
* @param array $role_data An array with role data.
*/
private function display_role_controls( $role_slug, $role_data ) {
static $excluded_options = false;
if ( false === $excluded_options ) {
$excluded_options = $this->get_role_manager_options();
}
?>
<div class="elementor-role-row <?php echo esc_attr( $role_slug ); ?>">
<div class="elementor-role-label">
<span class="elementor-role-name"><?php echo esc_html( $role_data['name'] ); ?></span>
<span data-excluded-label="<?php esc_attr_e( 'Role Excluded', 'elementor' ); ?>" class="elementor-role-excluded-indicator"></span>
<span class="elementor-role-toggle"><span class="dashicons dashicons-arrow-down"></span></span>
</div>
<div class="elementor-role-controls hidden">
<div class="elementor-role-control">
<label>
<input type="checkbox" name="elementor_exclude_user_roles[]" value="<?php echo esc_attr( $role_slug ); ?>"<?php checked( in_array( $role_slug, $excluded_options, true ), true ); ?>>
<?php echo __( 'No access to editor', 'elementor' ); ?>
</label>
</div>
<div>
<?php
/**
* Role restrictions controls.
*
* Fires after the role manager checkbox that allows the user to
* exclude the role.
*
* This filter allows developers to add custom controls to the role
* manager.
*
* @since 2.0.0
*
* @param string $role_slug The role slug.
* @param array $role_data An array with role data.
*/
do_action( 'elementor/role/restrictions/controls', $role_slug, $role_data );
?>
</div>
</div>
</div>
<?php
}
/**
* @since 2.0.0
* @access public
*/
public function get_go_pro_link_html() {
$pro_link = Utils::get_pro_link( 'https://elementor.com/pro/?utm_source=wp-role-manager&utm_campaign=gopro&utm_medium=wp-dash' );
?>
<div class="elementor-role-go-pro">
<div class="elementor-role-go-pro__desc"><?php echo __( 'Want to give access only to content?', 'elementor' ); ?></div>
<div class="elementor-role-go-pro__link"><a class="elementor-button elementor-button-default elementor-button-go-pro" target="_blank" href="<?php echo esc_url( $pro_link ); ?>"><?php echo __( 'Go Pro', 'elementor' ); ?></a></div>
</div>
<?php
}
/**
* @since 2.0.0
* @access public
*/
public function get_user_restrictions_array() {
$user = wp_get_current_user();
$user_roles = $user->roles;
$options = $this->get_user_restrictions();
$restrictions = [];
if ( empty( $options ) ) {
return $restrictions;
}
foreach ( $user_roles as $role ) {
if ( ! isset( $options[ $role ] ) ) {
continue;
}
$restrictions = array_merge( $restrictions, $options[ $role ] );
}
return array_unique( $restrictions );
}
/**
* @since 2.0.0
* @access private
*/
private function get_user_restrictions() {
static $restrictions = false;
if ( ! $restrictions ) {
$restrictions = [];
/**
* Editor user restrictions.
*
* Filters the user restrictions in the editor.
*
* @since 2.0.0
*
* @param array $restrictions User restrictions.
*/
$restrictions = apply_filters( 'elementor/editor/user/restrictions', $restrictions );
}
return $restrictions;
}
/**
* @since 2.0.0
* @access public
*
* @param $capability
*
* @return bool
*/
public function user_can( $capability ) {
$options = $this->get_user_restrictions_array();
if ( in_array( $capability, $options, true ) ) {
return false;
}
return true;
}
/**
* @since 2.0.0
* @access public
*/
public function __construct() {
parent::__construct();
add_action( 'admin_menu', [ $this, 'register_admin_menu' ], 100 );
add_action( 'elementor/role/restrictions/controls', [ $this, 'get_go_pro_link_html' ] );
}
}

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