PluginProbe
Elementor Website Builder – more than just a page builder / 3.8.0
Elementor Website Builder – more than just a page builder v3.8.0
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.8.0, at core/experiments/manager.php

823 lines 25.6 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_RC,
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_ALPHA,
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_import_export',
330 'title' => esc_html__( 'Import Export Website Kit', 'elementor' ),
331 'tag' => esc_html__( 'Feature', 'elementor' ),
332 'description' => esc_html__( 'Design sites faster with a Website Kit that contains some or all components of a complete site, like templates, content & site settings.', 'elementor' )
333 . '<br>'
334 . esc_html__( 'You can import a kit and apply it to your site, or export the elements from this site to be used anywhere else.', 'elementor' ),
335 'release_status' => self::RELEASE_STATUS_STABLE,
336 'default' => self::STATE_ACTIVE,
337 'new_site' => [
338 'default_active' => true,
339 'minimum_installation_version' => '3.2.0-beta',
340 ],
341 ] );
342
343 $this->add_feature( [
344 'name' => 'e_hidden_wordpress_widgets',
345 'title' => esc_html__( 'Hide native WordPress widgets from search results', 'elementor' ),
346 'tag' => esc_html__( 'Improvement', 'elementor' ),
347 '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' ),
348 'release_status' => self::RELEASE_STATUS_STABLE,
349 'default' => self::STATE_ACTIVE,
350 ] );
351
352 $this->add_feature( [
353 'name' => 'admin_menu_rearrangement',
354 'mutable' => false,
355 ] );
356
357 $this->add_feature( [
358 'name' => 'container',
359 'title' => esc_html__( 'Flexbox Container', 'elementor' ),
360 'tag' => esc_html__( 'Feature', 'elementor' ),
361 'description' => sprintf( esc_html__(
362 'Create advanced layouts and responsive designs with the new %1$sFlexbox Container element%2$s.
363 This experiment replaces the current section/column structure, but you\'ll still keep your existing
364 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.',
365 'elementor'
366 ), '<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>'),
367 'release_status' => self::RELEASE_STATUS_BETA,
368 'default' => self::STATE_INACTIVE,
369 ] );
370 }
371
372 /**
373 * Init States
374 *
375 * @since 3.1.0
376 * @access private
377 */
378 private function init_states() {
379 $this->states = [
380 self::STATE_DEFAULT => esc_html__( 'Default', 'elementor' ),
381 self::STATE_ACTIVE => esc_html__( 'Active', 'elementor' ),
382 self::STATE_INACTIVE => esc_html__( 'Inactive', 'elementor' ),
383 ];
384 }
385
386 /**
387 * Init Statuses
388 *
389 * @since 3.1.0
390 * @access private
391 */
392 private function init_release_statuses() {
393 $this->release_statuses = [
394 self::RELEASE_STATUS_DEV => esc_html__( 'Development', 'elementor' ),
395 self::RELEASE_STATUS_ALPHA => esc_html__( 'Alpha', 'elementor' ),
396 self::RELEASE_STATUS_BETA => esc_html__( 'Beta', 'elementor' ),
397 self::RELEASE_STATUS_RC => esc_html__( 'Release Candidate', 'elementor' ),
398 self::RELEASE_STATUS_STABLE => esc_html__( 'Stable', 'elementor' ),
399 ];
400 }
401
402 /**
403 * Init Features
404 *
405 * @since 3.1.0
406 * @access private
407 */
408 private function init_features() {
409 $this->features = [];
410
411 $this->add_default_features();
412
413 do_action( 'elementor/experiments/default-features-registered', $this );
414 }
415
416 /**
417 * Register Settings Fields
418 *
419 * @param Settings $settings
420 *
421 * @since 3.1.0
422 * @access private
423 *
424 */
425 private function register_settings_fields( Settings $settings ) {
426 $features = $this->get_features();
427
428 $fields = [];
429
430 foreach ( $features as $feature_name => $feature ) {
431 if ( ! $feature['mutable'] || $feature[ static::TYPE_HIDDEN ] ) {
432 unset( $features[ $feature_name ] );
433
434 continue;
435 }
436
437 $feature_key = 'experiment-' . $feature_name;
438
439 $section = 'stable' === $feature['release_status'] ? 'stable' : 'ongoing';
440
441 $fields[ $section ][ $feature_key ]['label'] = $this->get_feature_settings_label_html( $feature );
442
443 $fields[ $section ][ $feature_key ]['field_args'] = $feature;
444
445 $fields[ $section ][ $feature_key ]['render'] = function( $feature ) {
446 $this->render_feature_settings_field( $feature );
447 };
448 }
449
450 foreach ( [ 'stable', 'ongoing' ] as $section ) {
451 if ( ! isset( $fields[ $section ] ) ) {
452 $fields[ $section ]['no_features'] = [
453 'label' => esc_html__( 'No available experiments', 'elementor' ),
454 'field_args' => [
455 'type' => 'raw_html',
456 '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' ),
457 ],
458 ];
459 }
460
461 if ( ! Tracker::is_allow_track() && 'stable' === $section ) {
462 $fields[ $section ] += $settings->get_usage_fields();
463 }
464 }
465
466 $settings->add_tab(
467 'experiments', [
468 'label' => esc_html__( 'Experiments', 'elementor' ),
469 'sections' => [
470 'ongoing_experiments' => [
471 'callback' => function() {
472 $this->render_settings_intro();
473 },
474 'fields' => $fields['ongoing'],
475 ],
476 'stable_experiments' => [
477 'callback' => function() {
478 $this->render_stable_section_title();
479 },
480 'fields' => $fields['stable'],
481 ],
482 ],
483 ]
484 );
485 }
486
487 private function render_stable_section_title() {
488 ?>
489 <hr>
490 <h2>
491 <?php echo esc_html__( 'Stable Features', 'elementor' ); ?>
492 </h2>
493 <?php
494 }
495
496 /**
497 * Render Settings Intro
498 *
499 * @since 3.1.0
500 * @access private
501 */
502 private function render_settings_intro() {
503 ?>
504 <h2>
505 <?php echo esc_html__( 'Elementor Experiments', 'elementor' ); ?>
506 </h2>
507 <p class="e-experiment__description">
508 <?php
509 printf(
510 /* translators: %1$s Link open tag, %2$s: Link close tag. */
511 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' ),
512 '<a href="https://go.elementor.com/wp-dash-experiments/" target="_blank">',
513 '</a>'
514 );
515 ?>
516 </p>
517 <p class="e-experiment__description">
518 <?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' ); ?>
519 </p>
520 <p class="e-experiment__description">
521 <?php
522 printf(
523 /* translators: %1$s Link open tag, %2$s: Link close tag. */
524 esc_html__( 'Your feedback is important - %1$shelp us%2$s improve these features by sharing your thoughts and inputs.', 'elementor' ),
525 '<a href="https://go.elementor.com/wp-dash-experiments-report-an-issue/" target="_blank">',
526 '</a>'
527 );
528 ?>
529 </p>
530 <?php if ( $this->get_features() ) { ?>
531 <button type="button" class="button e-experiment__button" value="active">Activate All Experiments</button>
532 <button type="button" class="button e-experiment__button" value="inactive">Deactivate All Experiments</button>
533 <?php } ?>
534 <hr>
535 <h2 class="e-experiment__table-title">
536 <?php echo esc_html__( 'Ongoing Experiments', 'elementor' ); ?>
537 </h2>
538 <?php
539 }
540
541 /**
542 * Render Feature Settings Field
543 *
544 * @since 3.1.0
545 * @access private
546 *
547 * @param array $feature
548 */
549 private function render_feature_settings_field( array $feature ) {
550 $control_id = 'e-experiment-' . $feature['name'];
551 $control_name = $this->get_feature_option_key( $feature['name'] );
552
553 $status = sprintf(
554 esc_html__( 'Status: %s', 'elementor' ),
555 $this->release_statuses[ $feature['release_status'] ]
556 );
557
558 ?>
559 <div class="e-experiment__content">
560 <select class="e-experiment__select"
561 id="<?php echo esc_attr( $control_id ); ?>"
562 name="<?php echo esc_attr( $control_name ); ?>"
563 data-experiment-id="<?php echo esc_attr( $feature['name'] ); ?>"
564 >
565 <?php foreach ( $this->states as $state_key => $state_title ) { ?>
566 <option value="<?php echo esc_attr( $state_key ); ?>"
567 <?php selected( $state_key, $feature['state'] ); ?>
568 >
569 <?php echo esc_html( $state_title ); ?>
570 </option>
571 <?php } ?>
572 </select>
573
574 <p class="description">
575 <?php Utils::print_unescaped_internal_string( $feature['description'] ); ?>
576 </p>
577
578 <?php $this->render_feature_dependency( $feature ); ?>
579
580 <?php if ( 'stable' !== $feature['release_status'] ) { ?>
581 <div class="e-experiment__status">
582 <?php echo esc_html( $status ); ?>
583 </div>
584 <?php } ?>
585 </div>
586 <?php
587 }
588
589 private function render_feature_dependency( $feature ) {
590 $dependencies = ( new Collection( $feature['dependencies'] ?? [] ) )
591 ->map( function ( $dependency ) {
592 return $dependency->get_title();
593 } )
594 ->implode( ', ' );
595
596 if ( empty( $dependencies ) ) {
597 return;
598 }
599
600 ?>
601 <div class="e-experiment__dependency">
602 <strong class="e-experiment__dependency__title"><?php echo esc_html__( 'Requires', 'elementor' ); ?>:</strong>
603 <span><?php echo esc_html( $dependencies ); ?></span>
604 </div>
605 <?php
606 }
607
608 /**
609 * Get Feature Settings Label HTML
610 *
611 * @since 3.1.0
612 * @access private
613 *
614 * @param array $feature
615 *
616 * @return string
617 */
618 private function get_feature_settings_label_html( array $feature ) {
619 ob_start();
620
621 $is_feature_active = $this->is_feature_active( $feature['name'] );
622
623 $indicator_classes = 'e-experiment__title__indicator';
624
625 if ( $is_feature_active ) {
626 $indicator_classes .= ' e-experiment__title__indicator--active';
627 }
628
629 $indicator_tooltip = $this->get_feature_state_label( $feature );
630
631 ?>
632 <div class="e-experiment__title">
633 <div class="<?php echo $indicator_classes; ?>" data-tooltip="<?php echo $indicator_tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"></div>
634 <label class="e-experiment__title__label" for="e-experiment-<?php echo $feature['name']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo $feature['title']; ?></label>
635 <?php if ( ! empty( $feature['tag'] ) ) : ?>
636 <span class="e-experiment__title__tag"><?php echo $feature['tag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
637 <?php endif; ?>
638 </div>
639 <?php
640
641 return ob_get_clean();
642 }
643
644 /**
645 * Get Feature State Label
646 *
647 * @param array $feature
648 *
649 * @return string
650 */
651 public function get_feature_state_label( array $feature ) {
652 $is_feature_active = $this->is_feature_active( $feature['name'] );
653
654 if ( self::STATE_DEFAULT === $feature['state'] ) {
655 $label = $is_feature_active ? esc_html__( 'Active by default', 'elementor' ) :
656 esc_html__( 'Inactive by default', 'elementor' );
657 } else {
658 $label = self::STATE_ACTIVE === $feature['state'] ? esc_html__( 'Active', 'elementor' ) :
659 esc_html__( 'Inactive', 'elementor' );
660 }
661
662 return $label;
663 }
664
665 /**
666 * Get Feature Settings Label HTML
667 *
668 * @since 3.1.0
669 * @access private
670 *
671 * @param string $feature_name
672 *
673 * @return int
674 */
675 private function get_saved_feature_state( $feature_name ) {
676 return get_option( $this->get_feature_option_key( $feature_name ) );
677 }
678
679 /**
680 * Get Feature Actual State
681 *
682 * @since 3.1.0
683 * @access private
684 *
685 * @param array $feature
686 *
687 * @return string
688 */
689 private function get_feature_actual_state( array $feature ) {
690 if ( self::STATE_DEFAULT !== $feature['state'] ) {
691 return $feature['state'];
692 }
693
694 return $feature['default'];
695 }
696
697 /**
698 * On Feature State Change
699 *
700 * @since 3.1.0
701 * @access private
702 *
703 * @param array $old_feature_data
704 * @param string $new_state
705 *
706 * @throws \Elementor\Core\Experiments\Exceptions\Dependency_Exception
707 */
708 private function on_feature_state_change( array $old_feature_data, $new_state, $old_state ) {
709 $new_feature_data = $this->get_features( $old_feature_data['name'] );
710 $this->validate_dependency( $new_feature_data, $new_state );
711 $this->features[ $old_feature_data['name'] ]['state'] = $new_state;
712 if ( $old_state === $new_state ) {
713 return;
714 }
715
716 Plugin::$instance->files_manager->clear_cache();
717 if ( $new_feature_data['on_state_change'] ) {
718 $new_feature_data['on_state_change']( $old_state, $new_state );
719 }
720 }
721
722 /**
723 * @throws \Elementor\Core\Experiments\Exceptions\Dependency_Exception
724 */
725 private function validate_dependency( array $feature, $new_state ) {
726 $rollback = function ( $feature_option_key, $state ) {
727 remove_all_actions( 'add_option_' . $feature_option_key );
728 remove_all_actions( 'update_option_' . $feature_option_key );
729
730 update_option( $feature_option_key, $state );
731 };
732
733 if ( self::STATE_DEFAULT === $new_state ) {
734 $new_state = $this->get_feature_actual_state( $feature );
735 }
736
737 $feature_option_key = $this->get_feature_option_key( $feature['name'] );
738
739 if ( self::STATE_ACTIVE === $new_state ) {
740 if ( empty( $feature['dependencies'] ) ) {
741 return;
742 }
743
744 // Validate if the current feature dependency is available.
745 foreach ( $feature['dependencies'] as $dependency ) {
746 $dependency_feature = $this->get_features( $dependency->get_name() );
747
748 if ( ! $dependency_feature ) {
749 $rollback( $feature_option_key, self::STATE_INACTIVE );
750
751 throw new Exceptions\Dependency_Exception(
752 sprintf( 'The feature `%s` has a dependency `%s` that is not available.',
753 $feature['name'],
754 $dependency->get_name()
755 )
756 );
757 }
758
759 $dependency_state = $this->get_feature_actual_state( $dependency_feature );
760
761 // If dependency is not active.
762 if ( self::STATE_INACTIVE === $dependency_state ) {
763 $rollback( $feature_option_key, self::STATE_INACTIVE );
764
765 /* translators: 1: feature_name_that_change_state, 2: dependency_feature_name. */
766 throw new Exceptions\Dependency_Exception(
767 sprintf(
768 esc_html__( 'To turn on `%1$s`, Experiment: `%2$s` activity is required!', 'elementor' ),
769 $feature['name'],
770 $dependency_feature['name']
771 )
772 );
773 }
774 }
775 } elseif ( self::STATE_INACTIVE === $new_state ) {
776 // Make sure to deactivate a dependant experiment of the current feature when it's deactivated.
777 foreach ( $this->get_features() as $current_feature ) {
778 if ( empty( $current_feature['dependencies'] ) ) {
779 continue;
780 }
781
782 $current_feature_state = $this->get_feature_actual_state( $current_feature );
783
784 foreach ( $current_feature['dependencies'] as $dependency ) {
785 if ( self::STATE_ACTIVE === $current_feature_state && $feature['name'] === $dependency->get_name() ) {
786 update_option( $this->get_feature_option_key( $current_feature['name'] ), static::STATE_INACTIVE );
787 }
788 }
789 }
790 }
791 }
792
793 public function __construct() {
794 $this->init_states();
795
796 $this->init_release_statuses();
797
798 $this->init_features();
799
800 add_action( 'admin_init', function () {
801 System_Info::add_report(
802 'experiments', [
803 'file_name' => __DIR__ . '/experiments-reporter.php',
804 'class_name' => __NAMESPACE__ . '\Experiments_Reporter',
805 ]
806 );
807 }, 79 /* Before log */ );
808
809 if ( is_admin() ) {
810 $page_id = Settings::PAGE_ID;
811
812 add_action( "elementor/admin/after_create_settings/{$page_id}", function( Settings $settings ) {
813 $this->register_settings_fields( $settings );
814 }, 11 );
815 }
816
817 // Register CLI commands.
818 if ( Utils::is_wp_cli() ) {
819 \WP_CLI::add_command( 'elementor experiments', WP_CLI::class );
820 }
821 }
822 }
823