PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.4
Elementor Website Builder – more than just a page builder v4.2.4
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 / includes / utils.php

utils.php in Elementor Website Builder – more than just a page builder 4.2.4, at includes/utils.php

1,006 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;
3
4 use Elementor\Core\Files\Fonts\Google_Font;
5 use Elementor\Core\Utils\Collection;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit; // Exit if accessed directly.
9 }
10
11 /**
12 * Elementor utils.
13 *
14 * Elementor utils handler class is responsible for different utility methods
15 * used by Elementor.
16 *
17 * @since 1.0.0
18 */
19 class Utils {
20
21 const DEPRECATION_RANGE = 0.4;
22
23 const EDITOR_BREAK_LINES_OPTION_KEY = 'elementor_editor_break_lines';
24
25 /**
26 * A list of safe tags for `validate_html_tag` method.
27 */
28 const ALLOWED_HTML_WRAPPER_TAGS = [
29 'a',
30 'article',
31 'aside',
32 'button',
33 'form',
34 'div',
35 'footer',
36 'h1',
37 'h2',
38 'h3',
39 'h4',
40 'h5',
41 'h6',
42 'header',
43 'main',
44 'nav',
45 'p',
46 'section',
47 'span',
48 ];
49
50 const EXTENDED_ALLOWED_HTML_TAGS = [
51 'iframe' => [
52 'iframe' => [
53 'allow' => true,
54 'allowfullscreen' => true,
55 'frameborder' => true,
56 'height' => true,
57 'loading' => true,
58 'name' => true,
59 'referrerpolicy' => true,
60 'sandbox' => true,
61 'src' => true,
62 'width' => true,
63 ],
64 ],
65 'svg' => [
66 'svg' => [
67 'aria-hidden' => true,
68 'aria-labelledby' => true,
69 'class' => true,
70 'height' => true,
71 'role' => true,
72 'viewbox' => true,
73 'width' => true,
74 'xmlns' => true,
75 ],
76 'g' => [
77 'fill' => true,
78 ],
79 'title' => [
80 'title' => true,
81 ],
82 'path' => [
83 'd' => true,
84 'fill' => true,
85 ],
86 ],
87 'image' => [
88 'img' => [
89 'srcset' => true,
90 'sizes' => true,
91 ],
92 ],
93 ];
94
95 /**
96 * Variables for free to pro upsale modal promotions
97 */
98
99 const ANIMATED_HEADLINE = 'animated_headline';
100
101 const CTA = 'cta';
102
103 const VIDEO_PLAYLIST = 'video_playlist';
104
105 const TESTIMONIAL_WIDGET = 'testimonial_widget';
106
107 const IMAGE_CAROUSEL = 'image_carousel';
108
109 /**
110 * Whether WordPress CLI mode is enabled or not.
111 *
112 * @access public
113 * @static
114 *
115 * @return bool
116 */
117 public static function is_wp_cli() {
118 return defined( 'WP_CLI' ) && WP_CLI;
119 }
120
121 /**
122 * Whether script debug is enabled or not.
123 *
124 * @since 1.0.0
125 * @access public
126 * @static
127 *
128 * @return bool
129 */
130 public static function is_script_debug() {
131 return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
132 }
133
134 /**
135 * Whether Elementor debug is enabled or not.
136 *
137 * @access public
138 * @static
139 *
140 * @return bool
141 */
142 public static function is_elementor_debug() {
143 return defined( 'ELEMENTOR_DEBUG' ) && ELEMENTOR_DEBUG;
144 }
145
146 /**
147 * Whether Elementor test mode is enabled or not.
148 *
149 * @access public
150 * @static
151 *
152 * @return bool
153 */
154 public static function is_elementor_tests() {
155 return defined( 'ELEMENTOR_TESTS' ) && ELEMENTOR_TESTS;
156 }
157
158 /**
159 * Get pro link.
160 *
161 * Retrieve the link to Elementor Pro.
162 *
163 * @since 1.7.0
164 * @access public
165 * @static
166 *
167 * @param string $link URL to Elementor pro.
168 *
169 * @return string Elementor pro link.
170 */
171 public static function get_pro_link( $link ) {
172 static $theme_name = false;
173
174 if ( ! $theme_name ) {
175 $theme_obj = wp_get_theme();
176 if ( $theme_obj->parent() ) {
177 $theme_name = $theme_obj->parent()->get( 'Name' );
178 } else {
179 $theme_name = $theme_obj->get( 'Name' );
180 }
181
182 $theme_name = sanitize_key( $theme_name );
183 }
184
185 $link = add_query_arg( 'utm_term', $theme_name, $link );
186
187 return $link;
188 }
189
190 /**
191 * Replace URLs.
192 *
193 * Replace old URLs to new URLs. This method also updates all the Elementor data.
194 *
195 * @since 2.1.0
196 * @static
197 * @access public
198 *
199 * @param string $from
200 * @param string $to
201 *
202 * @return string
203 * @throws \Exception If URLs are missing or invalid URLs provided.
204 */
205 public static function replace_urls( $from, $to ) {
206 $from = trim( $from );
207 $to = trim( $to );
208
209 if ( empty( $from ) ) {
210 throw new \Exception( 'Couldn’t replace your address because the old URL was not provided. Try again by entering the old URL.' );
211 }
212
213 if ( empty( $to ) ) {
214 throw new \Exception( 'Couldn’t replace your address because the new URL was not provided. Try again by entering the new URL.' );
215 }
216
217 if ( $from === $to ) {
218 throw new \Exception( 'Couldn’t replace your address because both of the URLs provided are identical. Try again by entering different URLs.' );
219 }
220
221 $is_valid_urls = ( filter_var( $from, FILTER_VALIDATE_URL ) && filter_var( $to, FILTER_VALIDATE_URL ) );
222
223 if ( ! $is_valid_urls ) {
224 throw new \Exception( 'Couldn’t replace your address because at least one of the URLs provided are invalid. Try again by entering valid URLs.' );
225 }
226
227 global $wpdb;
228 $escaped_from = str_replace( '/', '\\/', $from );
229 $escaped_to = str_replace( '/', '\\/', $to );
230 $meta_value_like = '[%'; // meta_value LIKE '[%' are json formatted
231
232 $rows_affected = $wpdb->query(
233 $wpdb->prepare(
234 "UPDATE {$wpdb->postmeta} " .
235 'SET `meta_value` = REPLACE(`meta_value`, %s, %s) ' .
236 "WHERE `meta_key` = '_elementor_data' AND `meta_value` LIKE %s;",
237 $escaped_from,
238 $escaped_to,
239 $meta_value_like
240 )
241 );
242
243 if ( false === $rows_affected ) {
244 throw new \Exception( 'An error occurred while replacing URL\'s.' );
245 }
246
247 // Allow externals to replace-urls, when they have to.
248 $rows_affected += (int) apply_filters( 'elementor/tools/replace-urls', 0, $from, $to );
249
250 Plugin::$instance->files_manager->clear_cache();
251 Google_Font::clear_cache();
252
253 return sprintf(
254 /* translators: %d: Number of rows. */
255 _n( '%d database row affected.', '%d database rows affected.', $rows_affected, 'elementor' ),
256 $rows_affected
257 );
258 }
259
260 /**
261 * Is post supports Elementor.
262 *
263 * Whether the post supports editing with Elementor.
264 *
265 * @since 1.0.0
266 * @access public
267 * @static
268 *
269 * @param int $post_id Optional. Post ID. Default is `0`.
270 *
271 * @return string True if post supports editing with Elementor, false otherwise.
272 */
273 public static function is_post_support( $post_id = 0 ) {
274 $post_type = get_post_type( $post_id );
275
276 $is_supported = self::is_post_type_support( $post_type );
277
278 /**
279 * Is post type support.
280 *
281 * Filters whether the post type supports editing with Elementor.
282 *
283 * @since 1.0.0
284 * @deprecated 2.2.0 Use `elementor/utils/is_post_support` hook Instead.
285 *
286 * @param bool $is_supported Whether the post type supports editing with Elementor.
287 * @param int $post_id Post ID.
288 * @param string $post_type Post type.
289 */
290 $is_supported = apply_filters( 'elementor/utils/is_post_type_support', $is_supported, $post_id, $post_type );
291
292 /**
293 * Is post support.
294 *
295 * Filters whether the post supports editing with Elementor.
296 *
297 * @since 2.2.0
298 *
299 * @param bool $is_supported Whether the post type supports editing with Elementor.
300 * @param int $post_id Post ID.
301 * @param string $post_type Post type.
302 */
303 $is_supported = apply_filters( 'elementor/utils/is_post_support', $is_supported, $post_id, $post_type );
304
305 return $is_supported;
306 }
307
308
309 /**
310 * Is post type supports Elementor.
311 *
312 * Whether the post type supports editing with Elementor.
313 *
314 * @since 2.2.0
315 * @access public
316 * @static
317 *
318 * @param string $post_type Post Type.
319 *
320 * @return string True if post type supports editing with Elementor, false otherwise.
321 */
322 public static function is_post_type_support( $post_type ) {
323 if ( ! post_type_exists( $post_type ) ) {
324 return false;
325 }
326
327 if ( ! post_type_supports( $post_type, 'elementor' ) ) {
328 return false;
329 }
330
331 return true;
332 }
333
334 /**
335 * Get placeholder image source.
336 *
337 * Retrieve the source of the placeholder image.
338 *
339 * @since 1.0.0
340 * @access public
341 * @static
342 *
343 * @return string The source of the default placeholder image used by Elementor.
344 */
345 public static function get_placeholder_image_src() {
346 $placeholder_image = ELEMENTOR_ASSETS_URL . 'images/placeholder.png';
347
348 /**
349 * Get placeholder image source.
350 *
351 * Filters the source of the default placeholder image used by Elementor.
352 *
353 * @since 1.0.0
354 *
355 * @param string $placeholder_image The source of the default placeholder image.
356 */
357 $placeholder_image = apply_filters( 'elementor/utils/get_placeholder_image_src', $placeholder_image );
358
359 return $placeholder_image;
360 }
361
362 /**
363 * Generate random string.
364 *
365 * Returns a string containing a hexadecimal representation of random number.
366 *
367 * @since 1.0.0
368 * @access public
369 * @static
370 *
371 * @return string Random string.
372 */
373 public static function generate_random_string() {
374 return dechex( rand() );
375 }
376
377 /**
378 * Do not cache.
379 *
380 * Tell WordPress cache plugins not to cache this request.
381 *
382 * @since 1.0.0
383 * @access public
384 * @static
385 */
386 public static function do_not_cache() {
387 if ( ! defined( 'DONOTCACHEPAGE' ) ) {
388 define( 'DONOTCACHEPAGE', true );
389 }
390
391 if ( ! defined( 'DONOTCACHEDB' ) ) {
392 define( 'DONOTCACHEDB', true );
393 }
394
395 if ( ! defined( 'DONOTMINIFY' ) ) {
396 define( 'DONOTMINIFY', true );
397 }
398
399 if ( ! defined( 'DONOTCDN' ) ) {
400 define( 'DONOTCDN', true );
401 }
402
403 if ( ! defined( 'DONOTCACHEOBJECT' ) ) {
404 define( 'DONOTCACHEOBJECT', true );
405 }
406
407 // Set the headers to prevent caching for the different browsers.
408 nocache_headers();
409 }
410
411 /**
412 * Get timezone string.
413 *
414 * Retrieve timezone string from the WordPress database.
415 *
416 * @since 1.0.0
417 * @access public
418 * @static
419 *
420 * @return string Timezone string.
421 */
422 public static function get_timezone_string() {
423 $current_offset = (float) get_option( 'gmt_offset' );
424 $timezone_string = get_option( 'timezone_string' );
425
426 // Create a UTC+- zone if no timezone string exists.
427 if ( empty( $timezone_string ) ) {
428 if ( $current_offset < 0 ) {
429 $timezone_string = 'UTC' . $current_offset;
430 } else {
431 $timezone_string = 'UTC+' . $current_offset;
432 }
433 }
434
435 return $timezone_string;
436 }
437
438 /**
439 * Get create new post URL.
440 *
441 * Retrieve a custom URL for creating a new post/page using Elementor.
442 *
443 * @since 1.9.0
444 * @access public
445 * @deprecated 3.3.0 Use `Plugin::$instance->documents->get_create_new_post_url()` instead.
446 * @static
447 *
448 * @param string $post_type Optional. Post type slug. Default is 'page'.
449 * @param string|null $template_type Optional. Query arg 'template_type'. Default is null.
450 *
451 * @return string A URL for creating new post using Elementor.
452 */
453 public static function get_create_new_post_url( $post_type = 'page', $template_type = null ) {
454 Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __FUNCTION__, '3.3.0', 'Plugin::$instance->documents->get_create_new_post_url()' );
455
456 return Plugin::$instance->documents->get_create_new_post_url( $post_type, $template_type );
457 }
458
459 /**
460 * Get post autosave.
461 *
462 * Retrieve an autosave for any given post.
463 *
464 * @since 1.9.2
465 * @access public
466 * @static
467 *
468 * @param int $post_id Post ID.
469 * @param int $user_id Optional. User ID. Default is `0`.
470 *
471 * @return \WP_Post|false Post autosave or false.
472 */
473 public static function get_post_autosave( $post_id, $user_id = 0 ) {
474 global $wpdb;
475
476 $post = get_post( $post_id );
477
478 $where = $wpdb->prepare( 'post_parent = %d AND post_name LIKE %s AND post_modified_gmt > %s', [ $post_id, "{$post_id}-autosave%", $post->post_modified_gmt ] );
479
480 if ( $user_id ) {
481 $where .= $wpdb->prepare( ' AND post_author = %d', $user_id );
482 }
483
484 $revision = $wpdb->get_row( "SELECT * FROM $wpdb->posts WHERE $where AND post_type = 'revision'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
485
486 if ( $revision ) {
487 $revision = new \WP_Post( $revision );
488 } else {
489 $revision = false;
490 }
491
492 return $revision;
493 }
494
495 /**
496 * Is CPT supports custom templates.
497 *
498 * Whether the Custom Post Type supports templates.
499 *
500 * @since 2.0.0
501 * @access public
502 * @static
503 *
504 * @return bool True is templates are supported, False otherwise.
505 */
506 public static function is_cpt_custom_templates_supported() {
507 require_once ABSPATH . '/wp-admin/includes/theme.php';
508
509 return method_exists( wp_get_theme(), 'get_post_templates' );
510 }
511
512 /**
513 * @since 2.1.2
514 * @access public
515 * @static
516 */
517 public static function array_inject( $base_array, $key, $insert ) {
518 $length = array_search( $key, array_keys( $base_array ), true ) + 1;
519
520 return array_slice( $base_array, 0, $length, true ) +
521 $insert +
522 array_slice( $base_array, $length, null, true );
523 }
524
525 /**
526 * Render html attributes
527 *
528 * @access public
529 * @static
530 * @param array $attributes
531 *
532 * @return string
533 */
534 public static function render_html_attributes( array $attributes ) {
535 $rendered_attributes = [];
536
537 foreach ( $attributes as $attribute_key => $attribute_values ) {
538 if ( is_array( $attribute_values ) ) {
539 $attribute_values = implode( ' ', $attribute_values );
540 }
541
542 $rendered_attributes[] = sprintf( '%1$s="%2$s"', $attribute_key, esc_attr( $attribute_values ) );
543 }
544
545 return implode( ' ', $rendered_attributes );
546 }
547
548 /**
549 * Safe print html attributes
550 *
551 * @access public
552 * @static
553 * @param array $attributes
554 */
555 public static function print_html_attributes( array $attributes ) {
556 // PHPCS - the method render_html_attributes is safe.
557 echo self::render_html_attributes( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
558 }
559
560 public static function get_meta_viewport( $context = '' ) {
561 $meta_tag = '<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />';
562
563 /**
564 * Viewport meta tag.
565 *
566 * Filters the meta tag containing the viewport information.
567 *
568 * This hook can be used to change the initial viewport meta tag set by Elementor
569 * and replace it with a different viewport tag.
570 *
571 * @since 2.5.0
572 *
573 * @param string $meta_tag Viewport meta tag.
574 * @param string $context Page context.
575 */
576 $meta_tag = apply_filters( 'elementor/template/viewport_tag', $meta_tag, $context );
577
578 return $meta_tag;
579 }
580
581 /**
582 * Add Elementor Config js vars to the relevant script handle,
583 * WP will wrap it with <script> tag.
584 * To make sure this script runs thru the `script_loader_tag` hook, use a known handle value.
585 *
586 * @param string $handle
587 * @param string $js_var
588 * @param mixed $config
589 */
590 public static function print_js_config( $handle, $js_var, $config ) {
591 $config = wp_json_encode( $config );
592
593 if ( get_option( self::EDITOR_BREAK_LINES_OPTION_KEY ) ) {
594 // Add new lines to avoid memory limits in some hosting servers that handles the buffer output according to new line characters
595 $config = str_replace( '}},"', '}},' . PHP_EOL . '"', $config );
596 }
597
598 $script_data = 'var ' . $js_var . ' = ' . $config . ';';
599
600 wp_add_inline_script( $handle, $script_data, 'before' );
601 }
602
603 public static function handle_deprecation( $item, $version, $replacement = null ) {
604 preg_match( '/^[0-9]+\.[0-9]+/', ELEMENTOR_VERSION, $current_version );
605
606 $current_version_as_float = (float) $current_version[0];
607
608 preg_match( '/^[0-9]+\.[0-9]+/', $version, $alias_version );
609
610 $alias_version_as_float = (float) $alias_version[0];
611
612 if ( round( $current_version_as_float - $alias_version_as_float, 1 ) >= self::DEPRECATION_RANGE ) {
613 _deprecated_file( $item, $version, $replacement ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
614 }
615 }
616
617 /**
618 * Checks a control value for being empty, including a string of '0' not covered by PHP's empty().
619 *
620 * @param mixed $source
621 * @param bool|string $key
622 *
623 * @return bool
624 */
625 public static function is_empty( $source, $key = false ) {
626 if ( is_array( $source ) ) {
627 if ( ! isset( $source[ $key ] ) ) {
628 return true;
629 }
630
631 $source = $source[ $key ];
632 }
633
634 return '0' !== $source && empty( $source );
635 }
636
637 public static function has_pro() {
638 return defined( 'ELEMENTOR_PRO_VERSION' );
639 }
640
641 public static function is_license_active(): bool {
642 return class_exists( '\ElementorPro\License\API' ) && \ElementorPro\License\API::is_license_active();
643 }
644
645 public static function is_pro_installed_and_not_active(): bool {
646 if ( ! function_exists( 'get_plugins' ) ) {
647 require_once ABSPATH . 'wp-admin/includes/plugin.php';
648 }
649
650 $file_path = self::get_elementor_pro_file_path();
651 $installed_plugins = get_plugins();
652
653 return isset( $installed_plugins[ $file_path ] );
654 }
655
656 private static function get_elementor_pro_file_path(): string {
657 return 'elementor-pro/elementor-pro.php';
658 }
659
660 /**
661 * Convert HTMLEntities to UTF-8 characters
662 *
663 * @param string $html_string
664 * @return string
665 */
666 public static function urlencode_html_entities( $html_string ) {
667 $entities_dictionary = [
668 '&#145;' => "'", // Opening single quote
669 '&#146;' => "'", // Closing single quote
670 '&#147;' => '"', // Closing double quote
671 '&#148;' => '"', // Opening double quote
672 '&#8216;' => "'", // Closing single quote
673 '&#8217;' => "'", // Opening single quote
674 '&#8218;' => "'", // Single low quote
675 '&#8220;' => '"', // Closing double quote
676 '&#8221;' => '"', // Opening double quote
677 '&#8222;' => '"', // Double low quote
678 ];
679
680 // Decode decimal entities
681 $html_string = str_replace( array_keys( $entities_dictionary ), array_values( $entities_dictionary ), $html_string );
682
683 return rawurlencode( html_entity_decode( $html_string, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
684 }
685
686 /**
687 * Parse attributes that come as a string of comma-delimited key|value pairs.
688 * Removes Javascript events and unescaped `href` attributes.
689 *
690 * @param string $attributes_string
691 *
692 * @param string $delimiter Default comma `,`.
693 *
694 * @return array
695 */
696 public static function parse_custom_attributes( $attributes_string, $delimiter = ',' ) {
697 $attributes = explode( $delimiter, $attributes_string );
698 $result = [];
699
700 foreach ( $attributes as $attribute ) {
701 $attr_key_value = explode( '|', $attribute );
702
703 $attr_key = mb_strtolower( $attr_key_value[0] );
704
705 // Remove any not allowed characters.
706 preg_match( '/[-_a-z0-9]+/', $attr_key, $attr_key_matches );
707
708 if ( empty( $attr_key_matches[0] ) ) {
709 continue;
710 }
711
712 $attr_key = $attr_key_matches[0];
713
714 // Avoid Javascript events and unescaped href.
715 if ( 'href' === $attr_key || 'on' === substr( $attr_key, 0, 2 ) ) {
716 continue;
717 }
718
719 if ( isset( $attr_key_value[1] ) ) {
720 $attr_value = trim( $attr_key_value[1] );
721 } else {
722 $attr_value = '';
723 }
724
725 $result[ $attr_key ] = $attr_value;
726 }
727
728 return $result;
729 }
730
731 public static function find_element_recursive( $elements, $id ) {
732 foreach ( $elements as $element ) {
733 if ( $id === $element['id'] ) {
734 return $element;
735 }
736
737 $inner_elements = apply_filters(
738 'elementor/utils/find_element_recursive/inner_elements',
739 $element['elements'] ?? [],
740 $element
741 );
742
743 if ( ! empty( $inner_elements ) ) {
744 $found = self::find_element_recursive( $inner_elements, $id );
745
746 if ( $found ) {
747 return $found;
748 }
749 }
750 }
751
752 return false;
753 }
754
755 /**
756 * Change Submenu First Item Label
757 *
758 * Overwrite the label of the first submenu item of an admin menu item.
759 *
760 * Fired by `admin_menu` action.
761 *
762 * @since 3.1.0
763 *
764 * @param string $menu_slug
765 * @param string $new_label
766 * @access public
767 */
768 public static function change_submenu_first_item_label( $menu_slug, $new_label ) {
769 global $submenu;
770
771 if ( isset( $submenu[ $menu_slug ] ) ) {
772 // @codingStandardsIgnoreStart
773 $submenu[ $menu_slug ][0][0] = $new_label;
774 // @codingStandardsIgnoreEnd
775 }
776 }
777
778 /**
779 * Validate an HTML tag against a safe allowed list.
780 *
781 * @param string $tag
782 *
783 * @return string
784 */
785 public static function validate_html_tag( $tag ) {
786 return $tag && in_array( strtolower( $tag ), self::ALLOWED_HTML_WRAPPER_TAGS ) ? $tag : 'div';
787 }
788
789 /**
790 * Safe print a validated HTML tag.
791 *
792 * @param string $tag
793 */
794 public static function print_validated_html_tag( $tag ) {
795 // PHPCS - the method validate_html_tag is safe.
796 echo self::validate_html_tag( $tag ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
797 }
798
799 /**
800 * Print internal content (not user input) without escaping.
801 */
802 public static function print_unescaped_internal_string( $internal_string ) {
803 echo $internal_string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
804 }
805
806 /**
807 * Get recently edited posts query.
808 *
809 * Returns `WP_Query` of the recent edited posts.
810 * By default max posts ( $args['posts_per_page'] ) is 3.
811 *
812 * @param array $args
813 *
814 * @return \WP_Query
815 */
816 public static function get_recently_edited_posts_query( $args = [] ) {
817 $args = wp_parse_args( $args, [
818 'no_found_rows' => true,
819 'post_type' => 'any',
820 'post_status' => [ 'publish', 'draft' ],
821 'posts_per_page' => '3',
822 'meta_key' => '_elementor_edit_mode',
823 'meta_value' => 'builder',
824 'orderby' => 'modified',
825 ] );
826
827 return new \WP_Query( $args );
828 }
829
830 public static function print_wp_kses_extended( $text, array $tags ) {
831 $allowed_html = wp_kses_allowed_html( 'post' );
832
833 foreach ( $tags as $tag ) {
834 if ( isset( self::EXTENDED_ALLOWED_HTML_TAGS[ $tag ] ) ) {
835 $extended_tags = apply_filters( "elementor/extended_allowed_html_tags/{$tag}", self::EXTENDED_ALLOWED_HTML_TAGS[ $tag ] );
836 $allowed_html = array_replace_recursive( $allowed_html, $extended_tags );
837 }
838 }
839
840 echo wp_kses( $text, $allowed_html );
841 }
842
843 public static function kses_post_deep( $data ) {
844 return map_deep( $data, function ( $value ) {
845 return is_string( $value ) ? wp_kses_post( $value ) : $value;
846 } );
847 }
848
849 public static function is_elementor_path( $path ) {
850 $path = wp_normalize_path( $path );
851
852 /**
853 * Elementor related paths.
854 *
855 * Filters Elementor related paths.
856 *
857 * @param string[] $available_paths
858 */
859 $available_paths = apply_filters( 'elementor/utils/elementor_related_paths', [ ELEMENTOR_PATH ] );
860
861 return (bool) ( new Collection( $available_paths ) )
862 ->map( function ( $p ) {
863 // `untrailingslashit` in order to include other plugins prefixed with elementor.
864 return untrailingslashit( wp_normalize_path( $p ) );
865 } )
866 ->find(function ( $p ) use ( $path ) {
867 return false !== strpos( $path, $p );
868 } );
869 }
870
871 /**
872 * @param string $file
873 * @param mixed ...$args
874 * @return false|string
875 */
876 public static function file_get_contents( $file, ...$args ) {
877 if ( ! is_file( $file ) || ! is_readable( $file ) ) {
878 return false;
879 }
880 return file_get_contents( $file, ...$args );
881 }
882
883 public static function get_super_global_value( $super_global, $key ) {
884 if ( ! isset( $super_global[ $key ] ) ) {
885 return null;
886 }
887
888 if ( $_FILES === $super_global ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
889 return isset( $super_global[ $key ]['name'] ) ?
890 self::sanitize_file_name( $super_global[ $key ] ) :
891 self::sanitize_multi_upload( $super_global[ $key ] );
892 }
893
894 return wp_kses_post_deep( wp_unslash( $super_global[ $key ] ) );
895 }
896
897 private static function sanitize_multi_upload( $fields ) {
898 return array_map( function( $field ) {
899 return array_map( 'self::sanitize_file_name', $field );
900 }, $fields );
901 }
902
903 private static function sanitize_file_name( $file ) {
904 $file['name'] = sanitize_file_name( $file['name'] );
905
906 return $file;
907 }
908
909 /**
910 * Return specific object property value if exist from array of keys.
911 *
912 * @param array $base_array
913 * @param array $keys
914 * @return mixed|null
915 */
916 public static function get_array_value_by_keys( $base_array, $keys ) {
917 $keys = (array) $keys;
918 foreach ( $keys as $key ) {
919 if ( ! isset( $base_array[ $key ] ) ) {
920 return null;
921 }
922 $base_array = $base_array[ $key ];
923 }
924 return $base_array;
925 }
926
927 public static function get_cached_callback( $callback, $cache_key, $cache_time = 24 * HOUR_IN_SECONDS ) {
928 $cache = get_site_transient( $cache_key );
929
930 if ( ! $cache ) {
931 $cache = call_user_func( $callback );
932
933 if ( ! is_wp_error( $cache ) ) {
934 set_site_transient( $cache_key, $cache, $cache_time );
935 }
936 }
937
938 return $cache;
939 }
940
941 public static function is_sale_time(): bool {
942 $sale_start_time = gmmktime( 10, 0, 0, 6, 15, 2026 );
943 $sale_end_time = gmmktime( 3, 59, 0, 6, 17, 2026 );
944
945 $now_time = gmdate( 'U' );
946
947 return $now_time >= $sale_start_time && $now_time <= $sale_end_time;
948 }
949
950 public static function safe_throw( string $message ) {
951 if ( ! static::is_elementor_debug() ) {
952 return;
953 }
954
955 throw new \Exception( esc_html( $message ) );
956 }
957
958 public static function has_invalid_post_permissions( $post ): bool {
959 $is_image_attachment = 'attachment' === $post->post_type && strpos( $post->post_mime_type, 'image/' ) === 0;
960
961 if ( $is_image_attachment ) {
962 return false;
963 }
964
965 $is_private = 'private' === $post->post_status
966 && ! current_user_can( 'read_private_posts', $post->ID );
967
968 $not_allowed = 'publish' !== $post->post_status
969 && ! current_user_can( 'edit_post', $post->ID );
970
971 $password_required = post_password_required( $post->ID )
972 && ! current_user_can( 'edit_post', $post->ID );
973
974 return $is_private || $not_allowed || $password_required;
975 }
976
977 public static function is_custom_kit_applied() {
978 return (bool) Plugin::$instance->kits_manager->get_previous_id();
979 }
980
981 public static function decode_string( string $encoded_string, ?string $fallback = '' ) {
982 try {
983 return base64_decode( $encoded_string, true ) ?? $fallback;
984 } catch ( \Exception $e ) {
985 return $fallback;
986 }
987 }
988
989 public static function encode_string( string $decoded_string ): string {
990 return base64_encode( $decoded_string );
991 }
992
993 public static function html_to_plain_text( string $html ): string {
994 if ( empty( $html ) ) {
995 return '';
996 }
997
998 $text = preg_replace( '#<br\s*/?\s*>#i', ' ', $html );
999 $text = preg_replace( '#</?[a-z][^>]*>#i', ' ', $text );
1000 $text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );
1001 $text = str_replace( "\xE2\x80\x8B", '', $text );
1002
1003 return trim( preg_replace( '/\s+/', ' ', $text ) );
1004 }
1005 }
1006