PluginProbe
Elementor Website Builder – more than just a page builder / 3.10.1
Elementor Website Builder – more than just a page builder v3.10.1
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / core / experiments / manager.php

manager.php in Elementor Website Builder – more than just a page builder 3.10.1, at core/experiments/manager.php

818 lines 25.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Experiments;
3
4 use Elementor\Core\Base\Base_Object;
5 use Elementor\Core\Upgrade\Manager as Upgrade_Manager;
6 use Elementor\Core\Utils\Collection;
7 use Elementor\Modules\System_Info\Module as System_Info;
8 use Elementor\Plugin;
9 use Elementor\Settings;
10 use Elementor\Tracker;
11 use Elementor\Utils;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly
15 }
16
17 class Manager extends Base_Object {
18
19 const RELEASE_STATUS_DEV = 'dev';
20
21 const RELEASE_STATUS_ALPHA = 'alpha';
22
23 const RELEASE_STATUS_BETA = 'beta';
24
25 const RELEASE_STATUS_RC = 'rc';
26
27 const RELEASE_STATUS_STABLE = 'stable';
28
29 const STATE_DEFAULT = 'default';
30
31 const STATE_ACTIVE = 'active';
32
33 const STATE_INACTIVE = 'inactive';
34
35 const TYPE_HIDDEN = 'hidden';
36
37 private $states;
38
39 private $release_statuses;
40
41 private $features;
42
43 /**
44 * Add Feature
45 *
46 * @since 3.1.0
47 * @access public
48 *
49 * @param array $options {
50 * @type string $name
51 * @type string $title
52 * @type string $tag
53 * @type string $description
54 * @type string $release_status
55 * @type string $default
56 * @type callable $on_state_change
57 * }
58 *
59 * @return array|null
60 * @throws \Exception
61 */
62 public function add_feature( array $options ) {
63 if ( isset( $this->features[ $options['name'] ] ) ) {
64 return null;
65 }
66
67 $default_experimental_data = [
68 'tag' => '',
69 'description' => '',
70 'release_status' => self::RELEASE_STATUS_ALPHA,
71 'default' => self::STATE_INACTIVE,
72 'mutable' => true,
73 static::TYPE_HIDDEN => false,
74 'new_site' => [
75 'default_active' => false,
76 'always_active' => false,
77 'minimum_installation_version' => null,
78 ],
79 'on_state_change' => null,
80 ];
81
82 $allowed_options = [ 'name', 'title', 'tag', 'description', 'release_status', 'default', 'mutable', static::TYPE_HIDDEN, 'new_site', 'on_state_change', 'dependencies' ];
83
84 $experimental_data = $this->merge_properties( $default_experimental_data, $options, $allowed_options );
85
86 $new_site = $experimental_data['new_site'];
87
88 if ( $new_site['default_active'] || $new_site['always_active'] ) {
89 $is_new_installation = Upgrade_Manager::install_compare( $new_site['minimum_installation_version'], '>=' );
90
91 if ( $is_new_installation ) {
92 if ( $new_site['always_active'] ) {
93 $experimental_data['state'] = self::STATE_ACTIVE;
94
95 $experimental_data['mutable'] = false;
96 } elseif ( $new_site['default_active'] ) {
97 $experimental_data['default'] = self::STATE_ACTIVE;
98 }
99 }
100 }
101
102 if ( $experimental_data['mutable'] ) {
103 $experimental_data['state'] = $this->get_saved_feature_state( $options['name'] );
104 }
105
106 if ( empty( $experimental_data['state'] ) ) {
107 $experimental_data['state'] = self::STATE_DEFAULT;
108 }
109
110 if ( ! empty( $experimental_data['dependencies'] ) ) {
111 foreach ( $experimental_data['dependencies'] as $key => $dependency ) {
112 $feature = $this->get_features( $dependency );
113
114 if ( ! empty( $feature[ static::TYPE_HIDDEN ] ) ) {
115 throw new Exceptions\Dependency_Exception( 'Depending on a hidden experiment is not allowed.' );
116 }
117
118 if ( ! class_exists( $dependency ) && ! empty( $feature ) ) {
119 $experimental_data['dependencies'][ $key ] = new Wrap_Core_Dependency( $feature );
120 } else {
121 $experimental_data['dependencies'][ $key ] = $dependency::instance();
122 }
123 }
124 }
125
126 $this->features[ $options['name'] ] = $experimental_data;
127
128 if ( $experimental_data['mutable'] && is_admin() ) {
129 $feature_option_key = $this->get_feature_option_key( $options['name'] );
130
131 $on_state_change_callback = function( $old_state, $new_state ) use ( $experimental_data, $feature_option_key ) {
132 try {
133 $this->on_feature_state_change( $experimental_data, $new_state, $old_state );
134 } catch ( Exceptions\Dependency_Exception $e ) {
135 $message = sprintf(
136 '<p>%s</p><p><a href="#" onclick="location.href=\'%s\'">%s</a></p>',
137 esc_html( $e->getMessage() ),
138 site_url( 'wp-admin/admin.php?page=elementor#tab-experiments' ),
139 esc_html__( 'Back', 'elementor' )
140 );
141
142 wp_die( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
143 }
144 };
145
146 add_action( 'add_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
147 add_action( 'update_option_' . $feature_option_key, $on_state_change_callback, 10, 2 );
148 }
149
150 do_action( 'elementor/experiments/feature-registered', $this, $experimental_data );
151
152 return $experimental_data;
153 }
154
155 /**
156 * Remove Feature
157 *
158 * @since 3.1.0
159 * @access public
160 *
161 * @param string $feature_name
162 */
163 public function remove_feature( $feature_name ) {
164 unset( $this->features[ $feature_name ] );
165 }
166
167 /**
168 * Get Features
169 *
170 * @since 3.1.0
171 * @access public
172 *
173 * @param string $feature_name Optional. Default is null
174 *
175 * @return array|null
176 */
177 public function get_features( $feature_name = null ) {
178 return self::get_items( $this->features, $feature_name );
179 }
180
181 /**
182 * Get Active Features
183 *
184 * @since 3.1.0
185 * @access public
186 *
187 * @return array
188 */
189 public function get_active_features() {
190 return array_filter( $this->features, [ $this, 'is_feature_active' ], ARRAY_FILTER_USE_KEY );
191 }
192
193 /**
194 * Is Feature Active
195 *
196 * @since 3.1.0
197 * @access public
198 *
199 * @param string $feature_name
200 *
201 * @return bool
202 */
203 public function is_feature_active( $feature_name ) {
204 $feature = $this->get_features( $feature_name );
205
206 if ( ! $feature ) {
207 return false;
208 }
209
210 return self::STATE_ACTIVE === $this->get_feature_actual_state( $feature );
211 }
212
213 /**
214 * Set Feature Default State
215 *
216 * @since 3.1.0
217 * @access public
218 *
219 * @param string $feature_name
220 * @param string $default_state
221 */
222 public function set_feature_default_state( $feature_name, $default_state ) {
223 $feature = $this->get_features( $feature_name );
224
225 if ( ! $feature ) {
226 return;
227 }
228
229 $this->features[ $feature_name ]['default'] = $default_state;
230 }
231
232 /**
233 * Get Feature Option Key
234 *
235 * @since 3.1.0
236 * @access public
237 *
238 * @param string $feature_name
239 *
240 * @return string
241 */
242 public function get_feature_option_key( $feature_name ) {
243 return 'elementor_experiment-' . $feature_name;
244 }
245
246 private function add_default_features() {
247 $this->add_feature( [
248 'name' => 'e_dom_optimization',
249 'title' => esc_html__( 'Optimized DOM Output', 'elementor' ),
250 'tag' => esc_html__( 'Performance', 'elementor' ),
251 'description' => esc_html__( 'Developers, Please Note! This experiment includes some markup changes. If you\'ve used custom code in Elementor, you might have experienced a snippet of code not running. Turning this experiment off allows you to keep prior Elementor markup output settings, and have that lovely code running again.', 'elementor' )
252 . ' <a href="https://go.elementor.com/wp-dash-legacy-optimized-dom/" target="_blank">'
253 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
254 'release_status' => self::RELEASE_STATUS_STABLE,
255 'new_site' => [
256 'default_active' => true,
257 'minimum_installation_version' => '3.1.0-beta',
258 ],
259 ] );
260
261 $this->add_feature( [
262 'name' => 'e_optimized_assets_loading',
263 'title' => esc_html__( 'Improved Asset Loading', 'elementor' ),
264 'tag' => esc_html__( 'Performance', 'elementor' ),
265 'description' => esc_html__( 'Please Note! The "Improved Asset Loading" mode reduces the amount of code that is loaded on the page by default. When activated, parts of the infrastructure code will be loaded dynamically, only when needed. Keep in mind that activating this experiment may cause conflicts with incompatible plugins.', 'elementor' )
266 . ' <a href="https://go.elementor.com/wp-dash-improved-asset-loading/" target="_blank">'
267 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
268 'release_status' => self::RELEASE_STATUS_STABLE,
269 'new_site' => [
270 'default_active' => true,
271 'minimum_installation_version' => '3.2.0-beta',
272 ],
273 ] );
274
275 $this->add_feature( [
276 'name' => 'e_optimized_css_loading',
277 'title' => esc_html__( 'Improved CSS Loading', 'elementor' ),
278 'tag' => esc_html__( 'Performance', 'elementor' ),
279 'description' => esc_html__( 'Please Note! The “Improved CSS Loading” mode reduces the amount of CSS code that is loaded on the page by default. When activated, the CSS code will be loaded, rather inline or in a dedicated file, only when needed. Activating this experiment may cause conflicts with incompatible plugins.', 'elementor' )
280 . ' <a href="https://go.elementor.com/wp-dash-improved-css-loading/" target="_blank">'
281 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
282 'release_status' => self::RELEASE_STATUS_STABLE,
283 'new_site' => [
284 'default_active' => true,
285 'minimum_installation_version' => '3.3.0-beta',
286 ],
287 ] );
288
289 $this->add_feature( [
290 'name' => 'e_font_icon_svg',
291 'title' => esc_html__( 'Inline Font Icons', 'elementor' ),
292 'tag' => esc_html__( 'Performance', 'elementor' ),
293 'description' => 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' )
294 . ' <a href="https://go.elementor.com/wp-dash-inline-font-awesome/" target="_blank">'
295 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
296 'release_status' => self::RELEASE_STATUS_BETA,
297 ] );
298
299 $this->add_feature( [
300 'name' => 'a11y_improvements',
301 'title' => esc_html__( 'Accessibility Improvements', 'elementor' ),
302 'tag' => esc_html__( 'Performance', 'elementor' ),
303 'description' => esc_html__( 'An array of accessibility enhancements in Elementor pages.', 'elementor' )
304 . '<br><strong>' . esc_html__( 'Please note!', 'elementor' ) . '</strong> ' . esc_html__( 'These enhancements may include some markup changes to existing elementor widgets', 'elementor' )
305 . ' <a href="https://go.elementor.com/wp-dash-a11y-improvements/" target="_blank">'
306 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
307 'release_status' => self::RELEASE_STATUS_STABLE,
308 'new_site' => [
309 'default_active' => true,
310 'minimum_installation_version' => '3.1.0-beta',
311 ],
312 ] );
313
314 $this->add_feature( [
315 'name' => 'additional_custom_breakpoints',
316 'title' => esc_html__( 'Additional Custom Breakpoints', 'elementor' ),
317 'tag' => esc_html__( 'Performance', 'elementor' ),
318 'description' => 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' )
319 . ' <a href="https://go.elementor.com/wp-dash-additional-custom-breakpoints/" target="_blank">'
320 . esc_html__( 'Learn More', 'elementor' ) . '</a>',
321 'release_status' => self::RELEASE_STATUS_STABLE,
322 'new_site' => [
323 'default_active' => true,
324 'minimum_installation_version' => '3.4.0-beta',
325 ],
326 ] );
327
328 $this->add_feature( [
329 'name' => 'e_hidden_wordpress_widgets',
330 'title' => esc_html__( 'Hide native WordPress widgets from search results', 'elementor' ),
331 'tag' => esc_html__( 'Improvement', 'elementor' ),
332 'description' => esc_html__( 'WordPress widgets will not be shown when searching in the editor panel. Instead, these widgets can be found in the “WordPress” dropdown at the bottom of the panel.', 'elementor' ),
333 'release_status' => self::RELEASE_STATUS_STABLE,
334 'default' => self::STATE_ACTIVE,
335 ] );
336
337 $this->add_feature( [
338 'name' => 'admin_menu_rearrangement',
339 'mutable' => false,
340 ] );
341
342 $this->add_feature( [
343 'name' => 'container',
344 'title' => esc_html__( 'Flexbox Container', 'elementor' ),
345 'tag' => esc_html__( 'Feature', 'elementor' ),
346 'description' => sprintf( esc_html__(
347 'Create advanced layouts and responsive designs with the new %1$sFlexbox Container element%2$s.
348 This experiment replaces the current section/column structure, but you\'ll still keep your existing
349 Sections, Inner Sections and Columns and be able to edit them. Ready to give it a try? Check out the %3$sFlexbox playground%4$s.',
350 'elementor'
351 ), '<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container/">', '</a>', '<a target="_blank" href="https://go.elementor.com/wp-dash-flex-container-playground/">', '</a>'),
352 'release_status' => self::RELEASE_STATUS_BETA,
353 'default' => self::STATE_INACTIVE,
354 ] );
355 }
356
357 /**
358 * Init States
359 *
360 * @since 3.1.0
361 * @access private
362 */
363 private function init_states() {
364 $this->states = [
365 self::STATE_DEFAULT => esc_html__( 'Default', 'elementor' ),
366 self::STATE_ACTIVE => esc_html__( 'Active', 'elementor' ),
367 self::STATE_INACTIVE => esc_html__( 'Inactive', 'elementor' ),
368 ];
369 }
370
371 /**
372 * Init Statuses
373 *
374 * @since 3.1.0
375 * @access private
376 */
377 private function init_release_statuses() {
378 $this->release_statuses = [
379 self::RELEASE_STATUS_DEV => esc_html__( 'Development', 'elementor' ),
380 self::RELEASE_STATUS_ALPHA => esc_html__( 'Alpha', 'elementor' ),
381 self::RELEASE_STATUS_BETA => esc_html__( 'Beta', 'elementor' ),
382 self::RELEASE_STATUS_RC => esc_html__( 'Release Candidate', 'elementor' ),
383 self::RELEASE_STATUS_STABLE => esc_html__( 'Stable', 'elementor' ),
384 ];
385 }
386
387 /**
388 * Init Features
389 *
390 * @since 3.1.0
391 * @access private
392 */
393 private function init_features() {
394 $this->features = [];
395
396 $this->add_default_features();
397
398 do_action( 'elementor/experiments/default-features-registered', $this );
399 }
400
401 /**
402 * Register Settings Fields
403 *
404 * @param Settings $settings
405 *
406 * @since 3.1.0
407 * @access private
408 *
409 */
410 private function register_settings_fields( Settings $settings ) {
411 $features = $this->get_features();
412
413 $fields = [];
414
415 foreach ( $features as $feature_name => $feature ) {
416 $is_hidden = $feature[ static::TYPE_HIDDEN ];
417 $is_mutable = $feature['mutable'];
418 $should_hide_experiment = ! $is_mutable || ( $is_hidden && ! $this->should_show_hidden() );
419
420 if ( $should_hide_experiment ) {
421 unset( $features[ $feature_name ] );
422
423 continue;
424 }
425
426 $feature_key = 'experiment-' . $feature_name;
427
428 $section = 'stable' === $feature['release_status'] ? 'stable' : 'ongoing';
429
430 $fields[ $section ][ $feature_key ]['label'] = $this->get_feature_settings_label_html( $feature );
431
432 $fields[ $section ][ $feature_key ]['field_args'] = $feature;
433
434 $fields[ $section ][ $feature_key ]['render'] = function( $feature ) {
435 $this->render_feature_settings_field( $feature );
436 };
437 }
438
439 foreach ( [ 'stable', 'ongoing' ] as $section ) {
440 if ( ! isset( $fields[ $section ] ) ) {
441 $fields[ $section ]['no_features'] = [
442 'label' => esc_html__( 'No available experiments', 'elementor' ),
443 'field_args' => [
444 'type' => 'raw_html',
445 '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' ),
446 ],
447 ];
448 }
449
450 if ( ! Tracker::is_allow_track() && 'stable' === $section ) {
451 $fields[ $section ] += $settings->get_usage_fields();
452 }
453 }
454
455 $settings->add_tab(
456 'experiments', [
457 'label' => esc_html__( 'Experiments', 'elementor' ),
458 'sections' => [
459 'ongoing_experiments' => [
460 'callback' => function() {
461 $this->render_settings_intro();
462 },
463 'fields' => $fields['ongoing'],
464 ],
465 'stable_experiments' => [
466 'callback' => function() {
467 $this->render_stable_section_title();
468 },
469 'fields' => $fields['stable'],
470 ],
471 ],
472 ]
473 );
474 }
475
476 private function render_stable_section_title() {
477 ?>
478 <hr>
479 <h2>
480 <?php echo esc_html__( 'Stable Features', 'elementor' ); ?>
481 </h2>
482 <?php
483 }
484
485 /**
486 * Render Settings Intro
487 *
488 * @since 3.1.0
489 * @access private
490 */
491 private function render_settings_intro() {
492 ?>
493 <h2>
494 <?php echo esc_html__( 'Elementor Experiments', 'elementor' ); ?>
495 </h2>
496 <p class="e-experiment__description">
497 <?php
498 printf(
499 /* translators: %1$s Link open tag, %2$s: Link close tag. */
500 esc_html__( 'Access new and experimental features from Elementor before they\'re officially released. As these features are still in development, they are likely to change, evolve or even be removed altogether. %1$sLearn More.%2$s', 'elementor' ),
501 '<a href="https://go.elementor.com/wp-dash-experiments/" target="_blank">',
502 '</a>'
503 );
504 ?>
505 </p>
506 <p class="e-experiment__description">
507 <?php echo esc_html__( 'To use an experiment on your site, simply click on the dropdown next to it and switch to Active. You can always deactivate them at any time.', 'elementor' ); ?>
508 </p>
509 <p class="e-experiment__description">
510 <?php
511 printf(
512 /* translators: %1$s Link open tag, %2$s: Link close tag. */
513 esc_html__( 'Your feedback is important - %1$shelp us%2$s improve these features by sharing your thoughts and inputs.', 'elementor' ),
514 '<a href="https://go.elementor.com/wp-dash-experiments-report-an-issue/" target="_blank">',
515 '</a>'
516 );
517 ?>
518 </p>
519 <?php if ( $this->get_features() ) { ?>
520 <button type="button" class="button e-experiment__button" value="active"><?php echo esc_html__( 'Activate All Experiments', 'elementor' ); ?></button>
521 <button type="button" class="button e-experiment__button" value="inactive"><?php echo esc_html__( 'Deactivate All Experiments', 'elementor' ); ?></button>
522 <?php } ?>
523 <hr>
524 <h2 class="e-experiment__table-title">
525 <?php echo esc_html__( 'Ongoing Experiments', 'elementor' ); ?>
526 </h2>
527 <?php
528 }
529
530 /**
531 * Render Feature Settings Field
532 *
533 * @since 3.1.0
534 * @access private
535 *
536 * @param array $feature
537 */
538 private function render_feature_settings_field( array $feature ) {
539 $control_id = 'e-experiment-' . $feature['name'];
540 $control_name = $this->get_feature_option_key( $feature['name'] );
541
542 $status = sprintf(
543 esc_html__( 'Status: %s', 'elementor' ),
544 $this->release_statuses[ $feature['release_status'] ]
545 );
546
547 ?>
548 <div class="e-experiment__content">
549 <select class="e-experiment__select"
550 id="<?php echo esc_attr( $control_id ); ?>"
551 name="<?php echo esc_attr( $control_name ); ?>"
552 data-experiment-id="<?php echo esc_attr( $feature['name'] ); ?>"
553 >
554 <?php foreach ( $this->states as $state_key => $state_title ) { ?>
555 <option value="<?php echo esc_attr( $state_key ); ?>"
556 <?php selected( $state_key, $feature['state'] ); ?>
557 >
558 <?php echo esc_html( $state_title ); ?>
559 </option>
560 <?php } ?>
561 </select>
562
563 <p class="description">
564 <?php Utils::print_unescaped_internal_string( $feature['description'] ); ?>
565 </p>
566
567 <?php $this->render_feature_dependency( $feature ); ?>
568
569 <?php if ( 'stable' !== $feature['release_status'] ) { ?>
570 <div class="e-experiment__status">
571 <?php echo esc_html( $status ); ?>
572 </div>
573 <?php } ?>
574 </div>
575 <?php
576 }
577
578 private function render_feature_dependency( $feature ) {
579 $dependencies = ( new Collection( $feature['dependencies'] ?? [] ) )
580 ->map( function ( $dependency ) {
581 return $dependency->get_title();
582 } )
583 ->implode( ', ' );
584
585 if ( empty( $dependencies ) ) {
586 return;
587 }
588
589 ?>
590 <div class="e-experiment__dependency">
591 <strong class="e-experiment__dependency__title"><?php echo esc_html__( 'Requires', 'elementor' ); ?>:</strong>
592 <span><?php echo esc_html( $dependencies ); ?></span>
593 </div>
594 <?php
595 }
596
597 /**
598 * Get Feature Settings Label HTML
599 *
600 * @since 3.1.0
601 * @access private
602 *
603 * @param array $feature
604 *
605 * @return string
606 */
607 private function get_feature_settings_label_html( array $feature ) {
608 ob_start();
609
610 $is_feature_active = $this->is_feature_active( $feature['name'] );
611
612 $indicator_classes = 'e-experiment__title__indicator';
613
614 if ( $is_feature_active ) {
615 $indicator_classes .= ' e-experiment__title__indicator--active';
616 }
617
618 $indicator_tooltip = $this->get_feature_state_label( $feature );
619
620 ?>
621 <div class="e-experiment__title">
622 <div class="<?php echo $indicator_classes; ?>" data-tooltip="<?php echo $indicator_tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"></div>
623 <label class="e-experiment__title__label" for="e-experiment-<?php echo $feature['name']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo $feature['title']; ?></label>
624 <?php if ( ! empty( $feature['tag'] ) ) : ?>
625 <span class="e-experiment__title__tag"><?php echo $feature['tag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
626 <?php endif; ?>
627 </div>
628 <?php
629
630 return ob_get_clean();
631 }
632
633 /**
634 * Get Feature State Label
635 *
636 * @param array $feature
637 *
638 * @return string
639 */
640 public function get_feature_state_label( array $feature ) {
641 $is_feature_active = $this->is_feature_active( $feature['name'] );
642
643 if ( self::STATE_DEFAULT === $feature['state'] ) {
644 $label = $is_feature_active ? esc_html__( 'Active by default', 'elementor' ) :
645 esc_html__( 'Inactive by default', 'elementor' );
646 } else {
647 $label = self::STATE_ACTIVE === $feature['state'] ? esc_html__( 'Active', 'elementor' ) :
648 esc_html__( 'Inactive', 'elementor' );
649 }
650
651 return $label;
652 }
653
654 /**
655 * Get Feature Settings Label HTML
656 *
657 * @since 3.1.0
658 * @access private
659 *
660 * @param string $feature_name
661 *
662 * @return int
663 */
664 private function get_saved_feature_state( $feature_name ) {
665 return get_option( $this->get_feature_option_key( $feature_name ) );
666 }
667
668 /**
669 * Get Feature Actual State
670 *
671 * @since 3.1.0
672 * @access private
673 *
674 * @param array $feature
675 *
676 * @return string
677 */
678 private function get_feature_actual_state( array $feature ) {
679 if ( self::STATE_DEFAULT !== $feature['state'] ) {
680 return $feature['state'];
681 }
682
683 return $feature['default'];
684 }
685
686 /**
687 * On Feature State Change
688 *
689 * @since 3.1.0
690 * @access private
691 *
692 * @param array $old_feature_data
693 * @param string $new_state
694 *
695 * @throws \Elementor\Core\Experiments\Exceptions\Dependency_Exception
696 */
697 private function on_feature_state_change( array $old_feature_data, $new_state, $old_state ) {
698 $new_feature_data = $this->get_features( $old_feature_data['name'] );
699 $this->validate_dependency( $new_feature_data, $new_state );
700 $this->features[ $old_feature_data['name'] ]['state'] = $new_state;
701 if ( $old_state === $new_state ) {
702 return;
703 }
704
705 Plugin::$instance->files_manager->clear_cache();
706 if ( $new_feature_data['on_state_change'] ) {
707 $new_feature_data['on_state_change']( $old_state, $new_state );
708 }
709
710 do_action( 'elementor/experiments/feature-state-change/' . $old_feature_data['name'], $old_state, $new_state );
711 }
712
713 /**
714 * @throws \Elementor\Core\Experiments\Exceptions\Dependency_Exception
715 */
716 private function validate_dependency( array $feature, $new_state ) {
717 $rollback = function ( $feature_option_key, $state ) {
718 remove_all_actions( 'add_option_' . $feature_option_key );
719 remove_all_actions( 'update_option_' . $feature_option_key );
720
721 update_option( $feature_option_key, $state );
722 };
723
724 if ( self::STATE_DEFAULT === $new_state ) {
725 $new_state = $this->get_feature_actual_state( $feature );
726 }
727
728 $feature_option_key = $this->get_feature_option_key( $feature['name'] );
729
730 if ( self::STATE_ACTIVE === $new_state ) {
731 if ( empty( $feature['dependencies'] ) ) {
732 return;
733 }
734
735 // Validate if the current feature dependency is available.
736 foreach ( $feature['dependencies'] as $dependency ) {
737 $dependency_feature = $this->get_features( $dependency->get_name() );
738
739 if ( ! $dependency_feature ) {
740 $rollback( $feature_option_key, self::STATE_INACTIVE );
741
742 throw new Exceptions\Dependency_Exception(
743 sprintf(
744 'The feature `%s` has a dependency `%s` that is not available.',
745 $feature['name'],
746 $dependency->get_name()
747 )
748 );
749 }
750
751 $dependency_state = $this->get_feature_actual_state( $dependency_feature );
752
753 // If dependency is not active.
754 if ( self::STATE_INACTIVE === $dependency_state ) {
755 $rollback( $feature_option_key, self::STATE_INACTIVE );
756
757 throw new Exceptions\Dependency_Exception(
758 sprintf(
759 'To turn on `%1$s`, Experiment: `%2$s` activity is required!',
760 $feature['name'],
761 $dependency_feature['name']
762 )
763 );
764 }
765 }
766 } elseif ( self::STATE_INACTIVE === $new_state ) {
767 // Make sure to deactivate a dependant experiment of the current feature when it's deactivated.
768 foreach ( $this->get_features() as $current_feature ) {
769 if ( empty( $current_feature['dependencies'] ) ) {
770 continue;
771 }
772
773 $current_feature_state = $this->get_feature_actual_state( $current_feature );
774
775 foreach ( $current_feature['dependencies'] as $dependency ) {
776 if ( self::STATE_ACTIVE === $current_feature_state && $feature['name'] === $dependency->get_name() ) {
777 update_option( $this->get_feature_option_key( $current_feature['name'] ), static::STATE_INACTIVE );
778 }
779 }
780 }
781 }
782 }
783
784 private function should_show_hidden() {
785 return defined( 'ELEMENTOR_SHOW_HIDDEN_EXPERIMENTS' ) && ELEMENTOR_SHOW_HIDDEN_EXPERIMENTS;
786 }
787
788 public function __construct() {
789 $this->init_states();
790
791 $this->init_release_statuses();
792
793 $this->init_features();
794
795 add_action( 'admin_init', function () {
796 System_Info::add_report(
797 'experiments', [
798 'file_name' => __DIR__ . '/experiments-reporter.php',
799 'class_name' => __NAMESPACE__ . '\Experiments_Reporter',
800 ]
801 );
802 }, 79 /* Before log */ );
803
804 if ( is_admin() ) {
805 $page_id = Settings::PAGE_ID;
806
807 add_action( "elementor/admin/after_create_settings/{$page_id}", function( Settings $settings ) {
808 $this->register_settings_fields( $settings );
809 }, 11 );
810 }
811
812 // Register CLI commands.
813 if ( Utils::is_wp_cli() ) {
814 \WP_CLI::add_command( 'elementor experiments', WP_CLI::class );
815 }
816 }
817 }
818