balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

utils.php (19469B)


      1 <?php
      2 namespace Elementor;
      3 
      4 if ( ! defined( 'ABSPATH' ) ) {
      5 	exit; // Exit if accessed directly.
      6 }
      7 
      8 /**
      9  * Elementor utils.
     10  *
     11  * Elementor utils handler class is responsible for different utility methods
     12  * used by Elementor.
     13  *
     14  * @since 1.0.0
     15  */
     16 class Utils {
     17 
     18 	const DEPRECATION_RANGE = 0.4;
     19 
     20 	const EDITOR_BREAK_LINES_OPTION_KEY = 'elementor_editor_break_lines';
     21 
     22 	/**
     23 	 * A list of safe tage for `validate_html_tag` method.
     24 	 */
     25 	const ALLOWED_HTML_WRAPPER_TAGS = [
     26 		'a',
     27 		'article',
     28 		'aside',
     29 		'div',
     30 		'footer',
     31 		'h1',
     32 		'h2',
     33 		'h3',
     34 		'h4',
     35 		'h5',
     36 		'h6',
     37 		'header',
     38 		'main',
     39 		'nav',
     40 		'p',
     41 		'section',
     42 		'span',
     43 	];
     44 
     45 	const EXTENDED_ALLOWED_HTML_TAGS = [
     46 		'iframe' => [
     47 			'iframe' => [
     48 				'allow' => true,
     49 				'allowfullscreen' => true,
     50 				'frameborder' => true,
     51 				'height' => true,
     52 				'loading' => true,
     53 				'name' => true,
     54 				'referrerpolicy' => true,
     55 				'sandbox' => true,
     56 				'src' => true,
     57 				'width' => true,
     58 			],
     59 		],
     60 		'svg' => [
     61 			'svg' => [
     62 				'aria-hidden' => true,
     63 				'aria-labelledby' => true,
     64 				'class' => true,
     65 				'height' => true,
     66 				'role' => true,
     67 				'viewbox' => true,
     68 				'width' => true,
     69 				'xmlns' => true,
     70 			],
     71 			'g' => [
     72 				'fill' => true,
     73 			],
     74 			'title' => [
     75 				'title' => true,
     76 			],
     77 			'path' => [
     78 				'd' => true,
     79 				'fill' => true,
     80 			],
     81 		],
     82 		'image' => [
     83 			'img' => [
     84 				'srcset' => true,
     85 				'sizes' => true,
     86 			],
     87 		],
     88 	];
     89 
     90 	/**
     91 	 * Is ajax.
     92 	 *
     93 	 * Whether the current request is a WordPress ajax request.
     94 	 *
     95 	 * @since 1.0.0
     96 	 * @deprecated 2.6.0 Use `wp_doing_ajax()` instead.
     97 	 * @access public
     98 	 * @static
     99 	 *
    100 	 * @return bool True if it's a WordPress ajax request, false otherwise.
    101 	 */
    102 	public static function is_ajax() {
    103 		 _deprecated_function( __METHOD__, '2.6.0', 'wp_doing_ajax()' );
    104 
    105 		return wp_doing_ajax();
    106 	}
    107 
    108 	/**
    109 	 * Is WP CLI.
    110 	 *
    111 	 * @return bool
    112 	 */
    113 	public static function is_wp_cli() {
    114 		return defined( 'WP_CLI' ) && WP_CLI;
    115 	}
    116 
    117 	/**
    118 	 * Is script debug.
    119 	 *
    120 	 * Whether script debug is enabled or not.
    121 	 *
    122 	 * @since 1.0.0
    123 	 * @access public
    124 	 * @static
    125 	 *
    126 	 * @return bool True if it's a script debug is active, false otherwise.
    127 	 */
    128 	public static function is_script_debug() {
    129 		return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
    130 	}
    131 
    132 	/**
    133 	 * Get pro link.
    134 	 *
    135 	 * Retrieve the link to Elementor Pro.
    136 	 *
    137 	 * @since 1.7.0
    138 	 * @access public
    139 	 * @static
    140 	 *
    141 	 * @param string $link URL to Elementor pro.
    142 	 *
    143 	 * @return string Elementor pro link.
    144 	 */
    145 	public static function get_pro_link( $link ) {
    146 		static $theme_name = false;
    147 
    148 		if ( ! $theme_name ) {
    149 			$theme_obj = wp_get_theme();
    150 			if ( $theme_obj->parent() ) {
    151 				$theme_name = $theme_obj->parent()->get( 'Name' );
    152 			} else {
    153 				$theme_name = $theme_obj->get( 'Name' );
    154 			}
    155 
    156 			$theme_name = sanitize_key( $theme_name );
    157 		}
    158 
    159 		$link = add_query_arg( 'utm_term', $theme_name, $link );
    160 
    161 		return $link;
    162 	}
    163 
    164 	/**
    165 	 * Replace URLs.
    166 	 *
    167 	 * Replace old URLs to new URLs. This method also updates all the Elementor data.
    168 	 *
    169 	 * @since 2.1.0
    170 	 * @static
    171 	 * @access public
    172 	 *
    173 	 * @param $from
    174 	 * @param $to
    175 	 *
    176 	 * @return string
    177 	 * @throws \Exception
    178 	 */
    179 	public static function replace_urls( $from, $to ) {
    180 		$from = trim( $from );
    181 		$to = trim( $to );
    182 
    183 		if ( $from === $to ) {
    184 			throw new \Exception( esc_html__( 'The `from` and `to` URL\'s must be different', 'elementor' ) );
    185 		}
    186 
    187 		$is_valid_urls = ( filter_var( $from, FILTER_VALIDATE_URL ) && filter_var( $to, FILTER_VALIDATE_URL ) );
    188 		if ( ! $is_valid_urls ) {
    189 			throw new \Exception( esc_html__( 'The `from` and `to` URL\'s must be valid URL\'s', 'elementor' ) );
    190 		}
    191 
    192 		global $wpdb;
    193 
    194 		// @codingStandardsIgnoreStart cannot use `$wpdb->prepare` because it remove's the backslashes
    195 		$rows_affected = $wpdb->query(
    196 			"UPDATE {$wpdb->postmeta} " .
    197 			"SET `meta_value` = REPLACE(`meta_value`, '" . str_replace( '/', '\\\/', $from ) . "', '" . str_replace( '/', '\\\/', $to ) . "') " .
    198 			"WHERE `meta_key` = '_elementor_data' AND `meta_value` LIKE '[%' ;" ); // meta_value LIKE '[%' are json formatted
    199 		// @codingStandardsIgnoreEnd
    200 
    201 		if ( false === $rows_affected ) {
    202 			throw new \Exception( esc_html__( 'An error occurred', 'elementor' ) );
    203 		}
    204 
    205 		// Allow externals to replace-urls, when they have to.
    206 		$rows_affected += (int) apply_filters( 'elementor/tools/replace-urls', 0, $from, $to );
    207 
    208 		Plugin::$instance->files_manager->clear_cache();
    209 
    210 		return sprintf(
    211 			/* translators: %d: Number of rows */
    212 			_n( '%d row affected.', '%d rows affected.', $rows_affected, 'elementor' ),
    213 			$rows_affected
    214 		);
    215 	}
    216 
    217 	/**
    218 	 * Is post supports Elementor.
    219 	 *
    220 	 * Whether the post supports editing with Elementor.
    221 	 *
    222 	 * @since 1.0.0
    223 	 * @access public
    224 	 * @static
    225 	 *
    226 	 * @param int $post_id Optional. Post ID. Default is `0`.
    227 	 *
    228 	 * @return string True if post supports editing with Elementor, false otherwise.
    229 	 */
    230 	public static function is_post_support( $post_id = 0 ) {
    231 		$post_type = get_post_type( $post_id );
    232 
    233 		$is_supported = self::is_post_type_support( $post_type );
    234 
    235 		/**
    236 		 * Is post type support.
    237 		 *
    238 		 * Filters whether the post type supports editing with Elementor.
    239 		 *
    240 		 * @since 1.0.0
    241 		 * @deprecated 2.2.0 Use `elementor/utils/is_post_support` Instead
    242 		 *
    243 		 * @param bool $is_supported Whether the post type supports editing with Elementor.
    244 		 * @param int $post_id Post ID.
    245 		 * @param string $post_type Post type.
    246 		 */
    247 		$is_supported = apply_filters( 'elementor/utils/is_post_type_support', $is_supported, $post_id, $post_type );
    248 
    249 		/**
    250 		 * Is post support.
    251 		 *
    252 		 * Filters whether the post supports editing with Elementor.
    253 		 *
    254 		 * @since 2.2.0
    255 		 *
    256 		 * @param bool $is_supported Whether the post type supports editing with Elementor.
    257 		 * @param int $post_id Post ID.
    258 		 * @param string $post_type Post type.
    259 		 */
    260 		$is_supported = apply_filters( 'elementor/utils/is_post_support', $is_supported, $post_id, $post_type );
    261 
    262 		return $is_supported;
    263 	}
    264 
    265 
    266 	/**
    267 	 * Is post type supports Elementor.
    268 	 *
    269 	 * Whether the post type supports editing with Elementor.
    270 	 *
    271 	 * @since 2.2.0
    272 	 * @access public
    273 	 * @static
    274 	 *
    275 	 * @param string $post_type Post Type.
    276 	 *
    277 	 * @return string True if post type supports editing with Elementor, false otherwise.
    278 	 */
    279 	public static function is_post_type_support( $post_type ) {
    280 		if ( ! post_type_exists( $post_type ) ) {
    281 			return false;
    282 		}
    283 
    284 		if ( ! post_type_supports( $post_type, 'elementor' ) ) {
    285 			return false;
    286 		}
    287 
    288 		return true;
    289 	}
    290 
    291 	/**
    292 	 * Get placeholder image source.
    293 	 *
    294 	 * Retrieve the source of the placeholder image.
    295 	 *
    296 	 * @since 1.0.0
    297 	 * @access public
    298 	 * @static
    299 	 *
    300 	 * @return string The source of the default placeholder image used by Elementor.
    301 	 */
    302 	public static function get_placeholder_image_src() {
    303 		$placeholder_image = ELEMENTOR_ASSETS_URL . 'images/placeholder.png';
    304 
    305 		/**
    306 		 * Get placeholder image source.
    307 		 *
    308 		 * Filters the source of the default placeholder image used by Elementor.
    309 		 *
    310 		 * @since 1.0.0
    311 		 *
    312 		 * @param string $placeholder_image The source of the default placeholder image.
    313 		 */
    314 		$placeholder_image = apply_filters( 'elementor/utils/get_placeholder_image_src', $placeholder_image );
    315 
    316 		return $placeholder_image;
    317 	}
    318 
    319 	/**
    320 	 * Generate random string.
    321 	 *
    322 	 * Returns a string containing a hexadecimal representation of random number.
    323 	 *
    324 	 * @since 1.0.0
    325 	 * @access public
    326 	 * @static
    327 	 *
    328 	 * @return string Random string.
    329 	 */
    330 	public static function generate_random_string() {
    331 		return dechex( rand() );
    332 	}
    333 
    334 	/**
    335 	 * Do not cache.
    336 	 *
    337 	 * Tell WordPress cache plugins not to cache this request.
    338 	 *
    339 	 * @since 1.0.0
    340 	 * @access public
    341 	 * @static
    342 	 */
    343 	public static function do_not_cache() {
    344 		if ( ! defined( 'DONOTCACHEPAGE' ) ) {
    345 			define( 'DONOTCACHEPAGE', true );
    346 		}
    347 
    348 		if ( ! defined( 'DONOTCACHEDB' ) ) {
    349 			define( 'DONOTCACHEDB', true );
    350 		}
    351 
    352 		if ( ! defined( 'DONOTMINIFY' ) ) {
    353 			define( 'DONOTMINIFY', true );
    354 		}
    355 
    356 		if ( ! defined( 'DONOTCDN' ) ) {
    357 			define( 'DONOTCDN', true );
    358 		}
    359 
    360 		if ( ! defined( 'DONOTCACHCEOBJECT' ) ) {
    361 			define( 'DONOTCACHCEOBJECT', true );
    362 		}
    363 
    364 		// Set the headers to prevent caching for the different browsers.
    365 		nocache_headers();
    366 	}
    367 
    368 	/**
    369 	 * Get timezone string.
    370 	 *
    371 	 * Retrieve timezone string from the WordPress database.
    372 	 *
    373 	 * @since 1.0.0
    374 	 * @access public
    375 	 * @static
    376 	 *
    377 	 * @return string Timezone string.
    378 	 */
    379 	public static function get_timezone_string() {
    380 		$current_offset = (float) get_option( 'gmt_offset' );
    381 		$timezone_string = get_option( 'timezone_string' );
    382 
    383 		// Create a UTC+- zone if no timezone string exists.
    384 		if ( empty( $timezone_string ) ) {
    385 			if ( $current_offset < 0 ) {
    386 				$timezone_string = 'UTC' . $current_offset;
    387 			} else {
    388 				$timezone_string = 'UTC+' . $current_offset;
    389 			}
    390 		}
    391 
    392 		return $timezone_string;
    393 	}
    394 
    395 	/**
    396 	 * Get create new post URL.
    397 	 *
    398 	 * Retrieve a custom URL for creating a new post/page using Elementor.
    399 	 *
    400 	 * @since 1.9.0
    401 	 * @access public
    402 	 * @static
    403 	 *
    404 	 * @param string $post_type Optional. Post type slug. Default is 'page'.
    405 	 * @param string|null $template_type Optional. Query arg 'template_type'. Default is null.
    406 	 *
    407 	 * @return string A URL for creating new post using Elementor.
    408 	 */
    409 	public static function get_create_new_post_url( $post_type = 'page', $template_type = null ) {
    410 		Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __FUNCTION__, '3.3.0', 'Plugin::$instance->documents->get_create_new_post_url()' );
    411 
    412 		return Plugin::$instance->documents->get_create_new_post_url( $post_type, $template_type );
    413 	}
    414 
    415 	/**
    416 	 * Get post autosave.
    417 	 *
    418 	 * Retrieve an autosave for any given post.
    419 	 *
    420 	 * @since 1.9.2
    421 	 * @access public
    422 	 * @static
    423 	 *
    424 	 * @param int $post_id Post ID.
    425 	 * @param int $user_id Optional. User ID. Default is `0`.
    426 	 *
    427 	 * @return \WP_Post|false Post autosave or false.
    428 	 */
    429 	public static function get_post_autosave( $post_id, $user_id = 0 ) {
    430 		global $wpdb;
    431 
    432 		$post = get_post( $post_id );
    433 
    434 		$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 ] );
    435 
    436 		if ( $user_id ) {
    437 			$where .= $wpdb->prepare( ' AND post_author = %d', $user_id );
    438 		}
    439 
    440 		$revision = $wpdb->get_row( "SELECT * FROM $wpdb->posts WHERE $where AND post_type = 'revision'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    441 
    442 		if ( $revision ) {
    443 			$revision = new \WP_Post( $revision );
    444 		} else {
    445 			$revision = false;
    446 		}
    447 
    448 		return $revision;
    449 	}
    450 
    451 	/**
    452 	 * Is CPT supports custom templates.
    453 	 *
    454 	 * Whether the Custom Post Type supports templates.
    455 	 *
    456 	 * @since 2.0.0
    457 	 * @access public
    458 	 * @static
    459 	 *
    460 	 * @return bool True is templates are supported, False otherwise.
    461 	 */
    462 	public static function is_cpt_custom_templates_supported() {
    463 		require_once ABSPATH . '/wp-admin/includes/theme.php';
    464 
    465 		return method_exists( wp_get_theme(), 'get_post_templates' );
    466 	}
    467 
    468 	/**
    469 	 * @since 2.1.2
    470 	 * @access public
    471 	 * @static
    472 	 */
    473 	public static function array_inject( $array, $key, $insert ) {
    474 		$length = array_search( $key, array_keys( $array ), true ) + 1;
    475 
    476 		return array_slice( $array, 0, $length, true ) +
    477 			$insert +
    478 			array_slice( $array, $length, null, true );
    479 	}
    480 
    481 	/**
    482 	 * Render html attributes
    483 	 *
    484 	 * @access public
    485 	 * @static
    486 	 * @param array $attributes
    487 	 *
    488 	 * @return string
    489 	 */
    490 	public static function render_html_attributes( array $attributes ) {
    491 		$rendered_attributes = [];
    492 
    493 		foreach ( $attributes as $attribute_key => $attribute_values ) {
    494 			if ( is_array( $attribute_values ) ) {
    495 				$attribute_values = implode( ' ', $attribute_values );
    496 			}
    497 
    498 			$rendered_attributes[] = sprintf( '%1$s="%2$s"', $attribute_key, esc_attr( $attribute_values ) );
    499 		}
    500 
    501 		return implode( ' ', $rendered_attributes );
    502 	}
    503 
    504 	/**
    505 	 * Safe print html attributes
    506 	 *
    507 	 * @access public
    508 	 * @static
    509 	 * @param array $attributes
    510 	 */
    511 	public static function print_html_attributes( array $attributes ) {
    512 		// PHPCS - the method render_html_attributes is safe.
    513 		echo self::render_html_attributes( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    514 	}
    515 
    516 	public static function get_meta_viewport( $context = '' ) {
    517 		$meta_tag = '<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />';
    518 
    519 		/**
    520 		 * Viewport meta tag.
    521 		 *
    522 		 * Filters the meta tag containing the viewport information.
    523 		 *
    524 		 * This hook can be used to change the intial viewport meta tag set by Elementor
    525 		 * and replace it with a different viewport tag.
    526 		 *
    527 		 * @since 2.5.0
    528 		 *
    529 		 * @param string $meta_tag Viewport meta tag.
    530 		 * @param string $context  Page context.
    531 		 */
    532 		$meta_tag = apply_filters( 'elementor/template/viewport_tag', $meta_tag, $context );
    533 
    534 		return $meta_tag;
    535 	}
    536 
    537 	/**
    538 	 * Add Elementor Config js vars to the relevant script handle,
    539 	 * WP will wrap it with <script> tag.
    540 	 * To make sure this script runs thru the `script_loader_tag` hook, use a known handle value.
    541 	 * @param string $handle
    542 	 * @param string $js_var
    543 	 * @param mixed $config
    544 	 */
    545 	public static function print_js_config( $handle, $js_var, $config ) {
    546 		$config = wp_json_encode( $config );
    547 
    548 		if ( get_option( self::EDITOR_BREAK_LINES_OPTION_KEY ) ) {
    549 			// Add new lines to avoid memory limits in some hosting servers that handles the buffer output according to new line characters
    550 			$config = str_replace( '}},"', '}},' . PHP_EOL . '"', $config );
    551 		}
    552 
    553 		$script_data = 'var ' . $js_var . ' = ' . $config . ';';
    554 
    555 		wp_add_inline_script( $handle, $script_data, 'before' );
    556 	}
    557 
    558 	public static function handle_deprecation( $item, $version, $replacement = null ) {
    559 		preg_match( '/^[0-9]+\.[0-9]+/', ELEMENTOR_VERSION, $current_version );
    560 
    561 		$current_version_as_float = (float) $current_version[0];
    562 
    563 		preg_match( '/^[0-9]+\.[0-9]+/', $version, $alias_version );
    564 
    565 		$alias_version_as_float = (float) $alias_version[0];
    566 
    567 		if ( round( $current_version_as_float - $alias_version_as_float, 1 ) >= self::DEPRECATION_RANGE ) {
    568 			_deprecated_file( $item, $version, $replacement ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    569 		}
    570 	}
    571 
    572 	/**
    573 	 * Checks a control value for being empty, including a string of '0' not covered by PHP's empty().
    574 	 *
    575 	 * @param mixed $source
    576 	 * @param bool|string $key
    577 	 *
    578 	 * @return bool
    579 	 */
    580 	public static function is_empty( $source, $key = false ) {
    581 		if ( is_array( $source ) ) {
    582 			if ( ! isset( $source[ $key ] ) ) {
    583 				return true;
    584 			}
    585 
    586 			$source = $source[ $key ];
    587 		}
    588 
    589 		return '0' !== $source && empty( $source );
    590 	}
    591 
    592 	public static function has_pro() {
    593 		return defined( 'ELEMENTOR_PRO_VERSION' );
    594 	}
    595 
    596 	/**
    597 	 * Convert HTMLEntities to UTF-8 characters
    598 	 *
    599 	 * @param $string
    600 	 * @return string
    601 	 */
    602 	public static function urlencode_html_entities( $string ) {
    603 		$entities_dictionary = [
    604 			'&#145;' => "'", // Opening single quote
    605 			'&#146;' => "'", // Closing single quote
    606 			'&#147;' => '"', // Closing double quote
    607 			'&#148;' => '"', // Opening double quote
    608 			'&#8216;' => "'", // Closing single quote
    609 			'&#8217;' => "'", // Opening single quote
    610 			'&#8218;' => "'", // Single low quote
    611 			'&#8220;' => '"', // Closing double quote
    612 			'&#8221;' => '"', // Opening double quote
    613 			'&#8222;' => '"', // Double low quote
    614 		];
    615 
    616 		// Decode decimal entities
    617 		$string = str_replace( array_keys( $entities_dictionary ), array_values( $entities_dictionary ), $string );
    618 
    619 		return rawurlencode( html_entity_decode( $string, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
    620 	}
    621 
    622 	/**
    623 	 * Parse attributes that come as a string of comma-delimited key|value pairs.
    624 	 * Removes Javascript events and unescaped `href` attributes.
    625 	 *
    626 	 * @param string $attributes_string
    627 	 *
    628 	 * @param string $delimiter Default comma `,`.
    629 	 *
    630 	 * @return array
    631 	 */
    632 	public static function parse_custom_attributes( $attributes_string, $delimiter = ',' ) {
    633 		$attributes = explode( $delimiter, $attributes_string );
    634 		$result = [];
    635 
    636 		foreach ( $attributes as $attribute ) {
    637 			$attr_key_value = explode( '|', $attribute );
    638 
    639 			$attr_key = mb_strtolower( $attr_key_value[0] );
    640 
    641 			// Remove any not allowed characters.
    642 			preg_match( '/[-_a-z0-9]+/', $attr_key, $attr_key_matches );
    643 
    644 			if ( empty( $attr_key_matches[0] ) ) {
    645 				continue;
    646 			}
    647 
    648 			$attr_key = $attr_key_matches[0];
    649 
    650 			// Avoid Javascript events and unescaped href.
    651 			if ( 'href' === $attr_key || 'on' === substr( $attr_key, 0, 2 ) ) {
    652 				continue;
    653 			}
    654 
    655 			if ( isset( $attr_key_value[1] ) ) {
    656 				$attr_value = trim( $attr_key_value[1] );
    657 			} else {
    658 				$attr_value = '';
    659 			}
    660 
    661 			$result[ $attr_key ] = $attr_value;
    662 		}
    663 
    664 		return $result;
    665 	}
    666 
    667 	public static function find_element_recursive( $elements, $id ) {
    668 		foreach ( $elements as $element ) {
    669 			if ( $id === $element['id'] ) {
    670 				return $element;
    671 			}
    672 
    673 			if ( ! empty( $element['elements'] ) ) {
    674 				$element = self::find_element_recursive( $element['elements'], $id );
    675 
    676 				if ( $element ) {
    677 					return $element;
    678 				}
    679 			}
    680 		}
    681 
    682 		return false;
    683 	}
    684 
    685 	/**
    686 	 * Change Submenu First Item Label
    687 	 *
    688 	 * Overwrite the label of the first submenu item of an admin menu item.
    689 	 *
    690 	 * Fired by `admin_menu` action.
    691 	 *
    692 	 * @since 3.1.0
    693 	 *
    694 	 * @param $menu_slug
    695 	 * @param $new_label
    696 	 * @access public
    697 	 */
    698 	public static function change_submenu_first_item_label( $menu_slug, $new_label ) {
    699 		global $submenu;
    700 
    701 		if ( isset( $submenu[ $menu_slug ] ) ) {
    702 			// @codingStandardsIgnoreStart
    703 			$submenu[ $menu_slug ][0][0] = $new_label;
    704 			// @codingStandardsIgnoreEnd
    705 		}
    706 	}
    707 
    708 	/**
    709 	 * Validate an HTML tag against a safe allowed list.
    710 	 *
    711 	 * @param string $tag
    712 	 *
    713 	 * @return string
    714 	 */
    715 	public static function validate_html_tag( $tag ) {
    716 		return in_array( strtolower( $tag ), self::ALLOWED_HTML_WRAPPER_TAGS ) ? $tag : 'div';
    717 	}
    718 
    719 	/**
    720 	 * Safe print a validated HTML tag.
    721 	 *
    722 	 * @param string $tag
    723 	 */
    724 	public static function print_validated_html_tag( $tag ) {
    725 		// PHPCS - the method validate_html_tag is safe.
    726 		echo self::validate_html_tag( $tag ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    727 	}
    728 
    729 	/**
    730 	 * Print internal content (not user input) without escaping.
    731 	 */
    732 	public static function print_unescaped_internal_string( $string ) {
    733 		echo $string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    734 	}
    735 
    736 	/**
    737 	 * Get recently edited posts query.
    738 	 *
    739 	 * Returns `WP_Query` of the recent edited posts.
    740 	 * By default max posts ( $args['posts_per_page'] ) is 3.
    741 	 *
    742 	 * @param array $args
    743 	 *
    744 	 * @return \WP_Query
    745 	 */
    746 	public static function get_recently_edited_posts_query( $args = [] ) {
    747 		$args = wp_parse_args( $args, [
    748 			'post_type' => 'any',
    749 			'post_status' => [ 'publish', 'draft' ],
    750 			'posts_per_page' => '3',
    751 			'meta_key' => '_elementor_edit_mode',
    752 			'meta_value' => 'builder',
    753 			'orderby' => 'modified',
    754 		] );
    755 
    756 		return new \WP_Query( $args );
    757 	}
    758 
    759 	public static function print_wp_kses_extended( $string, array $tags ) {
    760 		$allowed_html = wp_kses_allowed_html( 'post' );
    761 		// Since PHP 5.6 cannot use isset() on the result of an expression.
    762 		$extended_allowed_html_tags = self::EXTENDED_ALLOWED_HTML_TAGS;
    763 
    764 		foreach ( $tags as $tag ) {
    765 			if ( isset( $extended_allowed_html_tags[ $tag ] ) ) {
    766 				$extended_tags = apply_filters( "elementor/extended_allowed_html_tags/{$tag}", self::EXTENDED_ALLOWED_HTML_TAGS[ $tag ] );
    767 				$allowed_html = array_replace_recursive( $allowed_html, $extended_tags );
    768 			}
    769 		}
    770 
    771 		echo wp_kses( $string, $allowed_html );
    772 	}
    773 }