| @@ -89,10 +89,8 @@ | ||
| 89 | 89 | } |
| 90 | 90 | |
| 91 | 91 | $this->features[ $options['name'] ] = $experimental_data; |
| 92 | 92 | |
| 93 | - $this->resolve_registered_dependants( $options['name'], $experimental_data ); | |
| 94 | - | |
| 95 | 93 | if ( $experimental_data['mutable'] && is_admin() ) { |
| 96 | 94 | $feature_option_key = $this->get_feature_option_key( $options['name'] ); |
| 97 | 95 | |
| 98 | 96 | $on_state_change_callback = function( $old_state, $new_state ) use ( $experimental_data, $feature_option_key ) { |
| @@ -122,11 +120,9 @@ | ||
| 122 | 120 | private function install_compare( $version ) { |
| 123 | 121 | $installs_history = Upgrade_Manager::get_installs_history(); |
| 124 | 122 | |
| 125 | 123 | if ( empty( $installs_history ) ) { |
| 126 | - // Fresh installation: upgrade manager hasn't written history yet on this first request. | |
| 127 | - // Use the current plugin version as the effective first-install version. | |
| 128 | - return version_compare( ELEMENTOR_VERSION, $version, '>=' ); | |
| 124 | + return false; | |
| 129 | 125 | } |
| 130 | 126 | |
| 131 | 127 | $cleaned_version = preg_replace( '/-(beta|cloud|dev)\d*$/', '', key( $installs_history ) ); |
| 132 | 128 | |
| @@ -251,11 +247,9 @@ | ||
| 251 | 247 | * |
| 252 | 248 | * @since 3.1.0 |
| 253 | 249 | * @access public |
| 254 | 250 | * |
| 255 | - * @param string $feature_name Experiment feature name. | |
| 256 | - * @param bool $check_dependencies When true, also require dependency experiments to be active. | |
| 257 | - * Missing dependencies always fail closed. Hidden dependencies are treated as satisfied. | |
| 251 | + * @param string $feature_name | |
| 258 | 252 | * |
| 259 | 253 | * @return bool |
| 260 | 254 | */ |
| 261 | 255 | public function is_feature_active( $feature_name, $check_dependencies = false ) { |
| @@ -264,36 +258,11 @@ | ||
| 264 | 258 | if ( ! $feature || self::STATE_ACTIVE !== $this->get_feature_actual_state( $feature ) ) { |
| 265 | 259 | return false; |
| 266 | 260 | } |
| 267 | 261 | |
| 268 | - if ( isset( $feature['dependencies'] ) && is_array( $feature['dependencies'] ) ) { | |
| 262 | + if ( $check_dependencies && isset( $feature['dependencies'] ) && is_array( $feature['dependencies'] ) ) { | |
| 269 | 263 | foreach ( $feature['dependencies'] as $dependency ) { |
| 270 | 264 | $dependent_feature = $this->get_features( $dependency->get_name() ); |
| 271 | - | |
| 272 | - // A dependency that Core still does not ship can never be verified as active. | |
| 273 | - // Non existing dependencies are re-resolved first to cover later registrations. | |
| 274 | - if ( $dependency instanceof Non_Existing_Dependency && ! $dependent_feature ) { | |
| 275 | - return false; | |
| 276 | - } | |
| 277 | - | |
| 278 | - if ( ! $dependent_feature ) { | |
| 279 | - // A Core experiment dependency that no longer resolves fails closed. | |
| 280 | - // Class dependencies are not experiment names and stay exempt. | |
| 281 | - if ( $dependency instanceof Wrap_Core_Dependency ) { | |
| 282 | - return false; | |
| 283 | - } | |
| 284 | - | |
| 285 | - continue; | |
| 286 | - } | |
| 287 | - | |
| 288 | - if ( ! $check_dependencies ) { | |
| 289 | - continue; | |
| 290 | - } | |
| 291 | - | |
| 292 | - if ( ! empty( $dependent_feature[ static::TYPE_HIDDEN ] ) ) { | |
| 293 | - continue; | |
| 294 | - } | |
| 295 | - | |
| 296 | 265 | $feature_state = self::STATE_ACTIVE === $this->get_feature_actual_state( $dependent_feature ); |
| 297 | 266 | |
| 298 | 267 | if ( ! $feature_state ) { |
| 299 | 268 | return false; |
| @@ -371,13 +340,14 @@ | ||
| 371 | 340 | $this->add_feature( [ |
| 372 | 341 | 'name' => 'container', |
| 373 | 342 | 'title' => esc_html__( 'Container', 'elementor' ), |
| 374 | 343 | 'description' => sprintf( |
| 375 | - /* translators: 1: Link opening tag, 2: Link closing tag, 3: Link opening tag, 4: Link closing tag */ | |
| 376 | - esc_html__( 'Create advanced layouts and responsive designs with %1$sFlexbox%2$s and %3$sGrid%4$s container elements.', 'elementor' ), | |
| 344 | + 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' ), | |
| 377 | 345 | '<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container/">', |
| 378 | 346 | '</a>', |
| 379 | 347 | '<a target="_blank" href="https://go.elementor.com/wp-dash-grid-container/">', |
| 348 | + '</a>', | |
| 349 | + '<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container-playground/">', | |
| 380 | 350 | '</a>' |
| 381 | 351 | ), |
| 382 | 352 | 'release_status' => self::RELEASE_STATUS_STABLE, |
| 383 | 353 | 'default' => self::STATE_INACTIVE, |
| @@ -398,24 +368,20 @@ | ||
| 398 | 368 | 'name' => 'e_optimized_markup', |
| 399 | 369 | 'title' => esc_html__( 'Optimized Markup', 'elementor' ), |
| 400 | 370 | 'tag' => esc_html__( 'Performance', 'elementor' ), |
| 401 | 371 | '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' ), |
| 402 | - 'release_status' => self::RELEASE_STATUS_STABLE, | |
| 372 | + 'release_status' => self::RELEASE_STATUS_BETA, | |
| 403 | 373 | 'default' => self::STATE_INACTIVE, |
| 404 | - 'new_site' => [ | |
| 405 | - 'default_active' => true, | |
| 406 | - 'minimum_installation_version' => '3.30.0', | |
| 407 | - ], | |
| 408 | 374 | ] ); |
| 409 | 375 | |
| 410 | 376 | $this->add_feature( [ |
| 411 | - 'name' => 'e_optimized_css_files', | |
| 412 | - 'title' => esc_html__( 'Optimized CSS Files', 'elementor' ), | |
| 377 | + 'name' => 'e_local_google_fonts', | |
| 378 | + 'title' => esc_html__( 'Load Google Fonts locally', 'elementor' ), | |
| 379 | + 'description' => esc_html__( "To improve page load performance and user privacy, replace Google Fonts CDN links with self-hosted font files. This approach downloads and serves font files directly from your server, eliminating external requests to Google's servers.", 'elementor' ), | |
| 413 | 380 | 'tag' => esc_html__( 'Performance', 'elementor' ), |
| 414 | - 'description' => esc_html__( 'Keeps external CSS files available and consistent for sites behind page caching or a CDN.', 'elementor' ), | |
| 415 | - 'release_status' => self::RELEASE_STATUS_ALPHA, | |
| 416 | - 'default' => self::STATE_INACTIVE, | |
| 381 | + 'release_status' => self::RELEASE_STATUS_STABLE, | |
| 417 | 382 | 'generator_tag' => true, |
| 383 | + 'default' => self::STATE_ACTIVE, | |
| 418 | 384 | ] ); |
| 419 | 385 | } |
| 420 | 386 | |
| 421 | 387 | /** |
| @@ -691,12 +657,9 @@ | ||
| 691 | 657 | <div class="e-experiment__title"> |
| 692 | 658 | <div class="<?php echo $indicator_classes; ?>" data-tooltip="<?php echo $indicator_tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"></div> |
| 693 | 659 | <label class="e-experiment__title__label" for="e-experiment-<?php echo $feature['name']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo $feature['title']; ?></label> |
| 694 | 660 | <?php foreach ( $feature['tags'] as $tag ) { ?> |
| 695 | - <?php | |
| 696 | - $tag_classes = 'e-experiment__title__tag e-experiment__title__tag__' . $tag['type'] . ' e-editor-one'; | |
| 697 | - ?> | |
| 698 | - <span class="<?php echo esc_attr( $tag_classes ); ?>"><?php echo $tag['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> | |
| 661 | + <span class="e-experiment__title__tag e-experiment__title__tag__<?php echo $tag['type']; ?>"><?php echo $tag['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> | |
| 699 | 662 | <?php } ?> |
| 700 | 663 | <?php if ( $feature['deprecated'] ) { ?> |
| 701 | 664 | <span class="e-experiment__title__tag e-experiment__title__tag__deprecated"><?php echo esc_html__( 'Deprecated', 'elementor' ); ?></span> |
| 702 | 665 | <?php } ?> |
| @@ -767,8 +730,10 @@ | ||
| 767 | 730 | * |
| 768 | 731 | * @param array $old_feature_data |
| 769 | 732 | * @param string $new_state |
| 770 | 733 | * @param string $old_state |
| 734 | + * | |
| 735 | + * @throws Dependency_Exception If the feature dependency is not available or not active. | |
| 771 | 736 | */ |
| 772 | 737 | private function on_feature_state_change( array $old_feature_data, $new_state, $old_state ) { |
| 773 | 738 | $new_feature_data = $this->get_features( $old_feature_data['name'] ); |
| 774 | 739 | $this->validate_dependency( $new_feature_data, $new_state ); |
| @@ -785,9 +750,9 @@ | ||
| 785 | 750 | do_action( 'elementor/experiments/feature-state-change/' . $old_feature_data['name'], $old_state, $new_state ); |
| 786 | 751 | } |
| 787 | 752 | |
| 788 | 753 | /** |
| 789 | - * @throws Exceptions\Dependency_Exception If the feature dependency is not available or not active. | |
| 754 | + * @throws Dependency_Exception If the feature dependency is not available or not active. | |
| 790 | 755 | */ |
| 791 | 756 | private function validate_dependency( array $feature, $new_state ) { |
| 792 | 757 | $rollback = function ( $feature_option_key, $state ) { |
| 793 | 758 | remove_all_actions( 'add_option_' . $feature_option_key ); |
| @@ -808,43 +773,22 @@ | ||
| 808 | 773 | } |
| 809 | 774 | |
| 810 | 775 | // Validate if the current feature dependency is available. |
| 811 | 776 | foreach ( $feature['dependencies'] as $dependency ) { |
| 812 | - if ( $dependency instanceof Non_Existing_Dependency ) { | |
| 813 | - $this->warn_removed_or_hidden_dependency( | |
| 814 | - sprintf( | |
| 815 | - 'The feature `%s` has a dependency `%s` that is not available in Core.', | |
| 816 | - esc_html( $feature['name'] ), | |
| 817 | - esc_html( $dependency->get_name() ) | |
| 818 | - ) | |
| 819 | - ); | |
| 820 | - continue; | |
| 821 | - } | |
| 822 | - | |
| 823 | 777 | $dependency_feature = $this->get_features( $dependency->get_name() ); |
| 824 | 778 | |
| 825 | 779 | if ( ! $dependency_feature ) { |
| 826 | - $this->warn_removed_or_hidden_dependency( | |
| 780 | + $rollback( $feature_option_key, self::STATE_INACTIVE ); | |
| 781 | + | |
| 782 | + throw new Exceptions\Dependency_Exception( | |
| 827 | 783 | sprintf( |
| 828 | - 'The feature `%s` has a dependency `%s` that is not available in Core.', | |
| 784 | + 'The feature `%s` has a dependency `%s` that is not available.', | |
| 829 | 785 | esc_html( $feature['name'] ), |
| 830 | 786 | esc_html( $dependency->get_name() ) |
| 831 | 787 | ) |
| 832 | 788 | ); |
| 833 | - continue; | |
| 834 | 789 | } |
| 835 | 790 | |
| 836 | - if ( $this->is_removed_or_hidden_dependency( $dependency_feature ) ) { | |
| 837 | - $this->warn_removed_or_hidden_dependency( | |
| 838 | - sprintf( | |
| 839 | - 'The feature `%1$s` depends on hidden experiment `%2$s`.', | |
| 840 | - esc_html( $feature['name'] ), | |
| 841 | - esc_html( $dependency_feature['name'] ) | |
| 842 | - ) | |
| 843 | - ); | |
| 844 | - continue; | |
| 845 | - } | |
| 846 | - | |
| 847 | 791 | $dependency_state = $this->get_feature_actual_state( $dependency_feature ); |
| 848 | 792 | |
| 849 | 793 | // If dependency is not active. |
| 850 | 794 | if ( self::STATE_INACTIVE === $dependency_state ) { |
| @@ -995,8 +939,10 @@ | ||
| 995 | 939 | |
| 996 | 940 | /** |
| 997 | 941 | * @param array $experimental_data |
| 998 | 942 | * @return array |
| 943 | + * | |
| 944 | + * @throws Dependency_Exception If the feature dependency is not initialized or depends on a hidden experiment. | |
| 999 | 945 | */ |
| 1000 | 946 | private function initialize_feature_dependencies( array $experimental_data ): array { |
| 1001 | 947 | foreach ( $experimental_data['dependencies'] as $key => $dependency ) { |
| 1002 | 948 | $feature = $this->get_features( $dependency ); |
| @@ -1001,28 +947,20 @@ | ||
| 1001 | 947 | foreach ( $experimental_data['dependencies'] as $key => $dependency ) { |
| 1002 | 948 | $feature = $this->get_features( $dependency ); |
| 1003 | 949 | |
| 1004 | 950 | if ( ! isset( $feature ) ) { |
| 1005 | - $this->warn_removed_or_hidden_dependency( | |
| 951 | + // since we must validate the state of each dependency, we have to make sure that dependencies are initialized in the correct order, otherwise, error. | |
| 952 | + throw new Exceptions\Dependency_Exception( | |
| 1006 | 953 | sprintf( |
| 1007 | - 'Feature %1$s depends on experiment %2$s that is not registered in Core.', | |
| 954 | + 'Feature %s cannot be initialized before dependency feature: %s.', | |
| 1008 | 955 | esc_html( $experimental_data['name'] ), |
| 1009 | 956 | esc_html( $dependency ) |
| 1010 | 957 | ) |
| 1011 | 958 | ); |
| 1012 | - | |
| 1013 | - $experimental_data['dependencies'][ $key ] = $this->create_dependency_class( $dependency, null ); | |
| 1014 | - continue; | |
| 1015 | 959 | } |
| 1016 | 960 | |
| 1017 | 961 | if ( ! empty( $feature[ static::TYPE_HIDDEN ] ) ) { |
| 1018 | - $this->warn_removed_or_hidden_dependency( | |
| 1019 | - sprintf( | |
| 1020 | - 'Feature %1$s depends on hidden experiment %2$s.', | |
| 1021 | - esc_html( $experimental_data['name'] ), | |
| 1022 | - esc_html( $dependency ) | |
| 1023 | - ) | |
| 1024 | - ); | |
| 962 | + throw new Exceptions\Dependency_Exception( 'Depending on a hidden experiment is not allowed.' ); | |
| 1025 | 963 | } |
| 1026 | 964 | |
| 1027 | 965 | $experimental_data['dependencies'][ $key ] = $this->create_dependency_class( $dependency, $feature ); |
| 1028 | 966 | $experimental_data = $this->set_feature_default_state_to_match_dependencies( $feature, $experimental_data ); |
| @@ -1028,48 +966,8 @@ | ||
| 1028 | 966 | $experimental_data = $this->set_feature_default_state_to_match_dependencies( $feature, $experimental_data ); |
| 1029 | 967 | } |
| 1030 | 968 | |
| 1031 | 969 | return $experimental_data; |
| 1032 | - } | |
| 1033 | - | |
| 1034 | - private function is_removed_or_hidden_dependency( $dependency_feature ): bool { | |
| 1035 | - if ( ! $dependency_feature ) { | |
| 1036 | - return true; | |
| 1037 | - } | |
| 1038 | - | |
| 1039 | - return ! empty( $dependency_feature[ static::TYPE_HIDDEN ] ); | |
| 1040 | - } | |
| 1041 | - | |
| 1042 | - /** | |
| 1043 | - * Re-resolve non existing dependency placeholders of dependants that were | |
| 1044 | - * registered before this feature existed. | |
| 1045 | - * | |
| 1046 | - * @param string $feature_name Newly registered feature name. | |
| 1047 | - * @param array $feature_data Newly registered feature data. | |
| 1048 | - */ | |
| 1049 | - private function resolve_registered_dependants( string $feature_name, array $feature_data ): void { | |
| 1050 | - foreach ( $this->features as $name => $registered_feature ) { | |
| 1051 | - if ( $name === $feature_name || empty( $registered_feature['dependencies'] ) || ! is_array( $registered_feature['dependencies'] ) ) { | |
| 1052 | - continue; | |
| 1053 | - } | |
| 1054 | - | |
| 1055 | - foreach ( $registered_feature['dependencies'] as $key => $dependency ) { | |
| 1056 | - if ( ! ( $dependency instanceof Non_Existing_Dependency ) || $dependency->get_name() !== $feature_name ) { | |
| 1057 | - continue; | |
| 1058 | - } | |
| 1059 | - | |
| 1060 | - $this->features[ $name ]['dependencies'][ $key ] = $this->create_dependency_class( $feature_name, $feature_data ); | |
| 1061 | - } | |
| 1062 | - } | |
| 1063 | - } | |
| 1064 | - | |
| 1065 | - private function warn_removed_or_hidden_dependency( string $message ): void { | |
| 1066 | - if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { | |
| 1067 | - return; | |
| 1068 | - } | |
| 1069 | - | |
| 1070 | - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Developer notice; message is escaped. | |
| 1071 | - _doing_it_wrong( __METHOD__, esc_html( $message ), ELEMENTOR_VERSION ); | |
| 1072 | 970 | } |
| 1073 | 971 | |
| 1074 | 972 | /** |
| 1075 | 973 | * @param array $feature |