$dependency_name ) ) { return $dependency_name::instance(); } if ( ! empty( $dependency_args ) ) { return new Wrap_Core_Dependency( $dependency_args ); } return new Non_Existing_Dependency( $dependency_name ); } /** * The experiments page is a WordPress options page, which means all the experiments are registered via WordPress' register_settings(), * and their states are being sent in the POST request when saving. * The options are being updated in a chronological order based on the POST data. * This behavior interferes with the experiments dependency mechanism because the data that's being sent can be in any order, * while the dependencies mechanism expects it to be in a specific order (dependencies should be activated before their dependents can). * In order to solve this issue, we sort the experiments in the POST data based on their dependencies tree. * * @param array $allowed_options */ private function sort_allowed_options_by_dependencies( $allowed_options ) { if ( ! isset( $allowed_options['elementor'] ) ) { return $allowed_options; } $sorted = Collection::make(); $visited = Collection::make(); $sort = function ( $item ) use ( &$sort, $sorted, $visited ) { if ( $visited->contains( $item ) ) { return; } $visited->push( $item ); $feature = $this->get_features( $item ); if ( ! $feature ) { return; } foreach ( $feature['dependencies'] ?? [] as $dep ) { $name = is_string( $dep ) ? $dep : $dep->get_name(); $sort( $name ); } $sorted->push( $item ); }; foreach ( $allowed_options['elementor'] as $option ) { $is_experiment_option = strpos( $option, static::OPTION_PREFIX ) === 0; if ( ! $is_experiment_option ) { continue; } $sort( str_replace( static::OPTION_PREFIX, '', $option ) ); } $allowed_options['elementor'] = Collection::make( $allowed_options['elementor'] ) ->filter( function ( $option ) { return 0 !== strpos( $option, static::OPTION_PREFIX ); } ) ->merge( $sorted->map( function ( $item ) { return static::OPTION_PREFIX . $item; } ) ) ->values(); return $allowed_options; } public function __construct() { $this->init_states(); $this->init_release_statuses(); $this->init_features(); add_action( 'admin_init', function () { System_Info::add_report( 'experiments', [ 'file_name' => __DIR__ . '/experiments-reporter.php', 'class_name' => __NAMESPACE__ . '\Experiments_Reporter', ] ); }, 79 /* Before log */ ); if ( is_admin() ) { $page_id = Settings::PAGE_ID; add_action( "elementor/admin/after_create_settings/{$page_id}", function( Settings $settings ) { $this->register_settings_fields( $settings ); }, 11 ); add_filter( 'allowed_options', function ( $allowed_options ) { return $this->sort_allowed_options_by_dependencies( $allowed_options ); }, 11 ); } // Register CLI commands. if ( Utils::is_wp_cli() ) { \WP_CLI::add_command( 'elementor experiments', WP_CLI::class ); } } /** * @param array $experimental_data * @return array * * @throws Exceptions\Dependency_Exception If the feature dependency is not initialized or depends on a hidden experiment. */ private function initialize_feature_dependencies( array $experimental_data ): array { foreach ( $experimental_data['dependencies'] as $key => $dependency ) { $feature = $this->get_features( $dependency ); if ( ! isset( $feature ) ) { // since we must validate the state of each dependency, we have to make sure that dependencies are initialized in the correct order, otherwise, error. throw new Exceptions\Dependency_Exception( sprintf( 'Feature %s cannot be initialized before dependency feature: %s.', esc_html( $experimental_data['name'] ), esc_html( $dependency ) ) ); } if ( ! empty( $feature[ static::TYPE_HIDDEN ] ) ) { throw new Exceptions\Dependency_Exception( 'Depending on a hidden experiment is not allowed.' ); } $experimental_data['dependencies'][ $key ] = $this->create_dependency_class( $dependency, $feature ); $experimental_data = $this->set_feature_default_state_to_match_dependencies( $feature, $experimental_data ); } return $experimental_data; } /** * @param array $feature * @param array $experimental_data * @return array * * we must validate the state: * * A user can set a dependant feature to inactive and in upgrade we don't change users settings. * * A developer can set the default state to be invalid (e.g. dependant feature is inactive). * if one of the dependencies is inactive, the main feature should be inactive as well. */ private function set_feature_default_state_to_match_dependencies( array $feature, array $experimental_data ): array { if ( self::STATE_INACTIVE !== $this->get_feature_actual_state( $feature ) ) { return $experimental_data; } if ( self::STATE_ACTIVE === $experimental_data['state'] ) { $experimental_data['state'] = self::STATE_INACTIVE; } elseif ( self::STATE_DEFAULT === $experimental_data['state'] ) { $experimental_data['default'] = self::STATE_INACTIVE; } return $experimental_data; } /** * @param array $new_site * @param array $experimental_data * @return array */ private function set_new_site_default_state( $new_site, array $experimental_data ): array { if ( ! $this->install_compare( $new_site['minimum_installation_version'] ) ) { return $experimental_data; } if ( $new_site['always_active'] ) { $experimental_data['state'] = self::STATE_ACTIVE; $experimental_data['mutable'] = false; } elseif ( $new_site['default_active'] ) { $experimental_data['default'] = self::STATE_ACTIVE; } elseif ( $new_site['default_inactive'] ) { $experimental_data['default'] = self::STATE_INACTIVE; } return $experimental_data; } /** * @param array $options * @return array */ private function set_feature_initial_options( array $options ): array { $default_experimental_data = [ 'tag' => '', // Deprecated, use 'tags' instead. 'tags' => [], 'description' => '', 'release_status' => self::RELEASE_STATUS_ALPHA, 'default' => self::STATE_INACTIVE, 'mutable' => true, static::TYPE_HIDDEN => false, 'new_site' => [ 'always_active' => false, 'default_active' => false, 'default_inactive' => false, 'minimum_installation_version' => null, ], 'on_state_change' => null, 'generator_tag' => false, 'deprecated' => false, ]; $allowed_options = [ 'name', 'title', 'tag', 'tags', 'description', 'release_status', 'default', 'mutable', static::TYPE_HIDDEN, 'new_site', 'on_state_change', 'dependencies', 'generator_tag', 'messages', 'deprecated' ]; $experimental_data = $this->merge_properties( $default_experimental_data, $options, $allowed_options ); return $this->unify_feature_tags( $experimental_data ); } }