namespace Elementor\Core\Experiments;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Experiments\Exceptions\Dependency_Exception;
use Elementor\Core\Upgrade\Manager as Upgrade_Manager;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\System_Info\Module as System_Info;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
class Manager extends Base_Object {
const RELEASE_STATUS_DEV = 'dev';
const RELEASE_STATUS_ALPHA = 'alpha';
const RELEASE_STATUS_BETA = 'beta';
const RELEASE_STATUS_STABLE = 'stable';
const STATE_DEFAULT = 'default';
const STATE_ACTIVE = 'active';
const STATE_INACTIVE = 'inactive';
const TYPE_HIDDEN = 'hidden';
const OPTION_PREFIX = 'elementor_experiment-';
private $release_statuses;
* Each feature has to provide the following information:
* 'description' => string,
* 'release_status' => string,
* @param array $options Feature options.
* @throws Dependency_Exception If can't change feature state.
public function add_feature( array $options ) {
if ( isset( $this->features[ $options['name'] ] ) ) {
$experimental_data = $this->set_feature_initial_options( $options );
$new_site = $experimental_data['new_site'];
if ( $new_site['default_active'] || $new_site['always_active'] || $new_site['default_inactive'] ) {
$experimental_data = $this->set_new_site_default_state( $new_site, $experimental_data );
if ( $experimental_data['mutable'] ) {
$experimental_data['state'] = $this->get_saved_feature_state( $options['name'] );
if ( empty( $experimental_data['state'] ) ) {
$experimental_data['state'] = self::STATE_DEFAULT;
if ( ! empty( $experimental_data['dependencies'] ) ) {
$experimental_data = $this->initialize_feature_dependencies( $experimental_data );
$this->features[ $options['name'] ] = $experimental_data;
if ( $experimental_data['mutable'] && is_admin() ) {
$feature_option_key = $this->get_feature_option_key( $options['name'] );
$on_state_change_callback = function( $old_state, $new_state ) use ( $experimental_data, $feature_option_key ) {
$this->on_feature_state_change( $experimental_data, $new_state, $old_state );
} catch ( Exceptions\Dependency_Exception $e ) {
'<p>%s</p><p><a href="#" onclick="location.href=\'%s\'">%s</a></p>',
esc_html( $e->getMessage() ),
Settings::get_settings_tab_url( 'experiments' ),
esc_html__( 'Back', 'elementor' )
wp_die( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
add_action( 'add_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
add_action( 'update_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
do_action( 'elementor/experiments/feature-registered', $this, $experimental_data );
return $experimental_data;
private function install_compare( $version ) {
$installs_history = Upgrade_Manager::get_installs_history();
if ( empty( $installs_history ) ) {
$cleaned_version = preg_replace( '/-(beta|cloud|dev)\d*$/', '', key( $installs_history ) );
* Combine 'tag' and 'tags' into one property.
* @param array $experimental_data
private function unify_feature_tags( array $experimental_data ): array {
foreach ( [ 'tag', 'tags' ] as $key ) {
if ( empty( $experimental_data[ $key ] ) ) {
$experimental_data[ $key ] = $this->format_feature_tags( $experimental_data[ $key ] );
if ( is_array( $experimental_data['tag'] ) ) {
$experimental_data['tags'] = array_merge( $experimental_data['tag'], $experimental_data['tags'] );
return $experimental_data;
* Format feature tags into the right format.
* If an array of tags provided, each tag has to provide the following information:
* @param string|array $tags A string of comma separated tags, or an array of tags.
private function format_feature_tags( $tags ): array {
if ( ! is_string( $tags ) && ! is_array( $tags ) ) {
$allowed_tag_properties = [ 'type', 'label' ];
// If $tags is string, explode by commas and convert to array.
if ( is_string( $tags ) ) {
$tags = array_filter( explode( ',', $tags ) );
foreach ( $tags as $i => $tag ) {
$tags[ $i ] = [ 'label' => trim( $tag ) ];
foreach ( $tags as $i => $tag ) {
if ( empty( $tag['label'] ) ) {
$tags[ $i ] = $this->merge_properties( $default_tag, $tag, $allowed_tag_properties );
* @param string $feature_name
public function remove_feature( $feature_name ) {
unset( $this->features[ $feature_name ] );
* @param string $feature_name Optional. Default is null.
public function get_features( $feature_name = null ) {
return self::get_items( $this->features, $feature_name );
public function get_active_features() {
return array_filter( $this->features, [ $this, 'is_feature_active' ], ARRAY_FILTER_USE_KEY );
* @param string $feature_name
public function is_feature_active( $feature_name, $check_dependencies = false ) {
$feature = $this->get_features( $feature_name );
if ( ! $feature || self::STATE_ACTIVE !== $this->get_feature_actual_state( $feature ) ) {
if ( $check_dependencies && isset( $feature['dependencies'] ) && is_array( $feature['dependencies'] ) ) {
foreach ( $feature['dependencies'] as $dependency ) {
$dependent_feature = $this->get_features( $dependency->get_name() );
$feature_state = self::STATE_ACTIVE === $this->get_feature_actual_state( $dependent_feature );
if ( ! $feature_state ) {
* Set Feature Default State
* @param string $feature_name
* @param string $default_state
public function set_feature_default_state( $feature_name, $default_state ) {
$feature = $this->get_features( $feature_name );
$this->features[ $feature_name ]['default'] = $default_state;
* @param string $feature_name
public function get_feature_option_key( $feature_name ) {
return static::OPTION_PREFIX . $feature_name;
private function add_default_features() {
'name' => 'e_font_icon_svg',
'title' => esc_html__( 'Inline Font Icons', 'elementor' ),
'tag' => esc_html__( 'Performance', 'elementor' ),
'description' => sprintf(
'%1$s <a href="https://go.elementor.com/wp-dash-inline-font-awesome/" target="_blank">%2$s</a>',
esc_html__( 'The “Inline Font Icons” will render the icons as inline SVG without loading the Font-Awesome and the eicons libraries and its related CSS files and fonts.', 'elementor' ),
esc_html__( 'Learn more', 'elementor' )
'release_status' => self::RELEASE_STATUS_STABLE,
'default_active' => true,
'minimum_installation_version' => '3.17.0',
'name' => 'additional_custom_breakpoints',
'title' => esc_html__( 'Additional Custom Breakpoints', 'elementor' ),
'description' => sprintf(
'%1$s <a href="https://go.elementor.com/wp-dash-additional-custom-breakpoints/" target="_blank">%2$s</a>',
esc_html__( 'Get pixel-perfect design for every screen size. You can now add up to 6 customizable breakpoints beyond the default desktop setting: mobile, mobile extra, tablet, tablet extra, laptop, and widescreen.', 'elementor' ),
esc_html__( 'Learn more', 'elementor' )
'release_status' => self::RELEASE_STATUS_STABLE,
'default' => self::STATE_ACTIVE,
'title' => esc_html__( 'Container', 'elementor' ),
'description' => sprintf(
/* translators: 1: Link opening tag, 2: Link closing tag, 3: Link opening tag, 4: Link closing tag, 5: Link opening tag, 6: Link closing tag */
esc_html__( 'Create advanced layouts and responsive designs with %1$sFlexbox%2$s and %3$sGrid%4$s container elements. Give it a try using the %5$sContainer playground%6$s.', 'elementor' ),
'<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container/">',
'<a target="_blank" href="https://go.elementor.com/wp-dash-grid-container/">',
'<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container-playground/">',
'release_status' => self::RELEASE_STATUS_STABLE,
'default' => self::STATE_INACTIVE,
'default_active' => true,
'minimum_installation_version' => '3.16.0',
'on_deactivate' => sprintf(
'%1$s <a target="_blank" href="https://go.elementor.com/wp-dash-deactivate-container/">%2$s</a>',
esc_html__( 'Container-based content will be hidden from your site and may not be recoverable in all cases.', 'elementor' ),
esc_html__( 'Learn more', 'elementor' ),
'name' => 'e_optimized_markup',
'title' => esc_html__( 'Optimized Markup', 'elementor' ),
'tag' => esc_html__( 'Performance', 'elementor' ),
'description' => esc_html__( 'Reduce the DOM size by eliminating HTML tags in various elements and widgets. This experiment includes markup changes so it might require updating custom CSS/JS code and cause compatibility issues with third party plugins.', 'elementor' ),
'release_status' => self::RELEASE_STATUS_STABLE,
'default' => self::STATE_INACTIVE,
'default_active' => true,
'minimum_installation_version' => '3.30.0',
private function init_states() {
self::STATE_DEFAULT => esc_html__( 'Default', 'elementor' ),
self::STATE_ACTIVE => esc_html__( 'Active', 'elementor' ),
self::STATE_INACTIVE => esc_html__( 'Inactive', 'elementor' ),
private function init_release_statuses() {
$this->release_statuses = [
self::RELEASE_STATUS_DEV => esc_html__( 'Development', 'elementor' ),
self::RELEASE_STATUS_ALPHA => esc_html__( 'Alpha', 'elementor' ),
self::RELEASE_STATUS_BETA => esc_html__( 'Beta', 'elementor' ),
self::RELEASE_STATUS_STABLE => esc_html__( 'Stable', 'elementor' ),
private function init_features() {
$this->add_default_features();
do_action( 'elementor/experiments/default-features-registered', $this );
* Register Settings Fields
* @param Settings $settings
private function register_settings_fields( Settings $settings ) {
$features = $this->get_features();
foreach ( $features as $feature_name => $feature ) {
$is_hidden = $feature[ static::TYPE_HIDDEN ];
$is_mutable = $feature['mutable'];
$should_hide_experiment = ! $is_mutable || ( $is_hidden && ! $this->should_show_hidden() ) || $this->has_non_existing_dependency( $feature );
if ( $should_hide_experiment ) {
unset( $features[ $feature_name ] );
$feature_key = 'experiment-' . $feature_name;
$section = 'stable' === $feature['release_status'] ? 'stable' : 'ongoing';
$fields[ $section ][ $feature_key ]['label'] = $this->get_feature_settings_label_html( $feature );
$fields[ $section ][ $feature_key ]['field_args'] = $feature;
$fields[ $section ][ $feature_key ]['render'] = function( $feature ) {
$this->render_feature_settings_field( $feature );
foreach ( [ 'stable', 'ongoing' ] as $section ) {
if ( ! isset( $fields[ $section ] ) ) {
$fields[ $section ]['no_features'] = [
'label' => esc_html__( 'No available experiments', 'elementor' ),
'html' => esc_html__( 'The current version of Elementor doesn\'t have any experimental features . if you\'re feeling curious make sure to come back in future versions.', 'elementor' ),
if ( ! Tracker::is_allow_track() && 'stable' === $section ) {
$fields[ $section ] += $settings->get_usage_fields();
'label' => esc_html__( 'Features', 'elementor' ),
'ongoing_experiments' => [
'callback' => function() {
$this->render_settings_intro();
'fields' => $fields['ongoing'],
'stable_experiments' => [
'callback' => function() {
$this->render_stable_section_title();
'fields' => $fields['stable'],
private function render_stable_section_title() {