/home/preegmxb/bricks.theoriginalsstudios.com/wp-content/themes/bricks/includes/templates.php
<?php
namespace Bricks;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

class Templates {
	public static $template_images = [];

	// All template IDs used on requested URL (@since 1.8.1)
	public static $rendered_template_ids_on_page = [];

	// All generated inline CSS identifiers (@since 1.9.1)
	public static $generated_inline_identifier = [];

	public function __construct() {
		add_filter( 'init', [ $this, 'register_post_type' ] );

		// Run on 'wp' and priority 20 to ensure set_active_templates && set_page_data in database.php ran first (@since 1.9.2)
		add_action( 'wp', [ $this, 'assign_templates_to_hooks' ], 20 );

		add_shortcode( 'bricks_template', [ $this, 'render_shortcode' ] );

		// Builder
		add_action( 'wp_ajax_bricks_create_template', [ $this, 'create_template' ] );
		add_action( 'wp_ajax_bricks_save_template', [ $this, 'save_template' ] );
		add_action( 'wp_ajax_bricks_delete_template', [ $this, 'delete_template' ] );

		// Admin & builder
		add_action( 'wp_ajax_bricks_import_template', [ $this, 'import_template' ] );
		add_action( 'wp_ajax_bricks_export_template', [ $this, 'export_template' ] );

		add_action( 'wp_ajax_bricks_convert_template', [ $this, 'convert_template' ] );

		add_action( 'save_post', [ $this, 'flush_templates_cache' ] );

		add_filter( 'wp_sitemaps_post_types', [ $this, 'remove_templates_from_wp_sitemap' ] );

		add_filter( 'wp_sitemaps_taxonomies', [ $this, 'remove_template_taxonomies_from_wp_sitemap' ] );
	}

	/**
	 * Register custom post types
	 *
	 * post_type: bricks_template
	 * taxonomies: template_tag, template_bundle
	 *
	 * @since 1.0
	 */
	public function register_post_type() {
		// Register post type: bricks_template
		register_post_type(
			BRICKS_DB_TEMPLATE_SLUG,
			[
				'labels'              => [
					'name'               => esc_html__( 'My Templates', 'bricks' ),
					'singular_name'      => esc_html__( 'Template', 'bricks' ),
					'add_new'            => esc_html__( 'Add New', 'bricks' ),
					'add_new_item'       => esc_html__( 'Add New Template', 'bricks' ),
					'edit_item'          => esc_html__( 'Edit Template', 'bricks' ),
					'new_item'           => esc_html__( 'New Template', 'bricks' ),
					'view_item'          => esc_html__( 'View Template', 'bricks' ),
					'view_items'         => esc_html__( 'View Templates', 'bricks' ),
					'search_items'       => esc_html__( 'Search Templates', 'bricks' ),
					'not_found'          => esc_html__( 'No Templates found', 'bricks' ),
					'not_found_in_trash' => esc_html__( 'No Template found in Trash', 'bricks' ),
					'all_items'          => esc_html__( 'All Templates', 'bricks' ),
					'menu_name'          => esc_html__( 'My Templates', 'bricks' ),
				],
				'public'              => true,
				'rewrite'             => [ 'slug' => 'template' ],
				/**
				 * Exclude Bricks templates from search resuls on the frontend if Bricks setting "Public Templates" is not enabled
				 *
				 * @since 1.9.3
				 */
				'exclude_from_search' => ! bricks_is_builder() && ! Database::get_setting( 'publicTemplates', false ),
				'hierarchical'        => false,
				'show_in_menu'        => false,
				'show_in_nav_menus'   => true,
				'capability_type'     => 'post',
				'supports'            => [
					'author',
					'revisions',
					'thumbnail',
					'title',
				],
				'taxonomies'          => [ BRICKS_DB_TEMPLATE_TAX_TAG ],
			]
		);

		// Register template taxomony: 'template_tag'
		register_taxonomy(
			BRICKS_DB_TEMPLATE_TAX_TAG,
			BRICKS_DB_TEMPLATE_SLUG,
			[
				'labels' => [
					'name'          => esc_html__( 'Template Tags', 'bricks' ),
					'singular_name' => esc_html__( 'Template Tag', 'bricks' ),
					'all_items'     => esc_html__( 'All Template Tags', 'bricks' ),
					'edit_item'     => esc_html__( 'Edit Template Tag', 'bricks' ),
					'view_item'     => esc_html__( 'View Template Tag', 'bricks' ),
					'update_item'   => esc_html__( 'Update Template Tag', 'bricks' ),
					'add_new_item'  => esc_html__( 'Add New Template Tag', 'bricks' ),
					'new_item_name' => esc_html__( 'New Template Name', 'bricks' ),
					'search_items'  => esc_html__( 'Search Template Tags', 'bricks' ),
					'not_found'     => esc_html__( 'No Template Tag found', 'bricks' ),
					'name'          => esc_html__( 'Template Tag', 'bricks' ),
				],
			]
		);

		// Register template taxomony: 'template_bundle'
		register_taxonomy(
			BRICKS_DB_TEMPLATE_TAX_BUNDLE,
			BRICKS_DB_TEMPLATE_SLUG,
			[
				'labels' => [
					'name'          => esc_html__( 'Template Bundles', 'bricks' ),
					'singular_name' => esc_html__( 'Template Bundle', 'bricks' ),
					'all_items'     => esc_html__( 'All Template Bundles', 'bricks' ),
					'edit_item'     => esc_html__( 'Edit Template Bundle', 'bricks' ),
					'view_item'     => esc_html__( 'View Template Bundle', 'bricks' ),
					'update_item'   => esc_html__( 'Update Template Bundle', 'bricks' ),
					'add_new_item'  => esc_html__( 'Add New Template Bundle', 'bricks' ),
					'new_item_name' => esc_html__( 'New Template Name', 'bricks' ),
					'search_items'  => esc_html__( 'Search Template Bundles', 'bricks' ),
					'not_found'     => esc_html__( 'No Template Bundle found', 'bricks' ),
					'name'          => esc_html__( 'Template Bundle', 'bricks' ),
				],
			]
		);
	}

	/**
	 * Render shortcode: [bricks_template]
	 */
	public function render_shortcode( $attributes = [] ) {
		$template_id = ! empty( $attributes['id'] ) ? intval( $attributes['id'] ) : false;
		// @since 1.9.1 - To indicate that the shortcode is rendered on the hook, might need to add inline styles later
		$is_on_hook = ! empty( $attributes['on_hook'] ) ? true : false;

		if ( ! $template_id ) {
			return;
		}

		$original_post_id = ! empty( Database::$page_data['original_post_id'] ) ? Database::$page_data['original_post_id'] : '';

		// post_id at this stage could be template preview post ID (populated content)
		$post_id = get_the_ID();

		// Avoid loops: Shortcode rendering inside of itself
		// Ensure $original_post_id is a bricks template when use for comparison, it might be a term ID (#862k7jcn7)
		if ( $template_id == $post_id || ( Helpers::is_bricks_template( $original_post_id ) && $template_id == $original_post_id ) ) {
			return Helpers::get_element_placeholder(
				[
					'title' => esc_html__( 'Not allowed: Infinite template loop.', 'bricks' ),
				]
			);
		}

		$elements = get_post_meta( $template_id, BRICKS_DB_PAGE_CONTENT, true );

		if ( empty( $elements ) || ! is_array( $elements ) ) {
			return Helpers::get_element_placeholder(
				[
					'title' => esc_html__( 'Your selected template is empty.', 'bricks' ),
				]
			);
		}

		$html = '';

		/**
		 * STEP: Generate template CSS (builder, inline styles or external files)
		 *
		 * Non-loop templates only as in-loop CSS is generated in assets.php line 2535
		 */

		// Collect all rendered template IDs for requested URL (@since 1.8.1)
		if ( ! in_array( $template_id, self::$rendered_template_ids_on_page ) ) {
			self::$rendered_template_ids_on_page[] = $template_id;
		}

		// Check for icon fonts and global elements
		Assets::enqueue_setting_specific_scripts( $elements );

		$template_inline_css = self::generate_inline_css( $template_id, $elements );

		// STEP: Builder (append template CSS as inline <style> to element HTML)
		if ( bricks_is_builder() || bricks_is_builder_call() ) {
			// Use 'data-template-id' to get template ID in builder to generate global classes CSS of Template element (@since 1.8.2)
			$template_inline_css .= Assets::$inline_css_dynamic_data;
			$html                .= "<style data-template-id=\"{$template_id}\" id=\"bricks-inline-css-template-{$template_id}\">{$template_inline_css}</style>";
		}

		// STEP: CSS loading method: External files
		elseif ( Database::get_setting( 'cssLoading' ) === 'file' ) {
			$template_css_file_dir = Assets::$css_dir . "/post-$template_id.min.css";
			$template_css_file_url = Assets::$css_url . "/post-$template_id.min.css";

			if ( file_exists( $template_css_file_dir ) ) {
				wp_enqueue_style( "bricks-post-$template_id", $template_css_file_url, [], filemtime( $template_css_file_dir ) );
			}

			// When assign section template to hook, some ID level styles are missing when using external files and is looping (@since 1.9.1)
			if ( $is_on_hook && Query::is_any_looping() ) {
				Assets::$inline_css_dynamic_data .= $template_inline_css;
			}
		}

		// STEP: CSS loading method: Inline styles (default)
		else {
			// Get dynamic data styles to add as inline CSS on the frontend (@since 1.8.2)
			Assets::$inline_css_dynamic_data .= $template_inline_css;
		}

		// STEP: Avoid infinite template loops
		static $rendered_shortcode_template_ids = [];

		if ( ! in_array( $template_id, $rendered_shortcode_template_ids ) ) {
			// Add template ID to avoid infinite loops (reset below after template has been rendered)
			$rendered_shortcode_template_ids[] = $template_id;

			// Store the current main render_data self::$elements
			$store_elements = Frontend::$elements;

			$html .= Frontend::render_data( $elements );

			// Reset the main render_data self::$elements
			Frontend::$elements = $store_elements;

			// Reset template ID by removing last template ID from the array
			array_pop( $rendered_shortcode_template_ids );
		}

		/**
		 * Build looping popup HTML (render in footer)
		 *
		 * @since 1.7.1
		 */
		if ( self::get_template_type( $template_id ) === 'popup' ) {
			Popups::build_looping_popup_html( $template_id );

			return;
		}

		return $html;
	}

	/**
	 * Generate the inline CSS for template rendered in shortcode element
	 */
	public static function generate_inline_css( $template_id, $elements ) {
		if ( empty( $template_id ) ) {
			return;
		}

		// Return: Template has not been published (@since 1.7.1)
		if ( $template_id && get_post_status( $template_id ) !== 'publish' ) {
			return;
		}

		$inline_css = '';

		Assets::generate_css_from_elements( $elements, "template_$template_id" );

		// Check as template_{id} is not set when using inline CSS loading method (see template.php line 77)
		$template_inline_css = Assets::$inline_css[ "template_$template_id" ] ?? '';

		if ( $template_inline_css ) {

			$looping_query_id = Query::is_any_looping();

			if ( $looping_query_id ) {
				$unique_loop_id = [
					$template_id,
					Query::get_query_element_id( $looping_query_id ),
					Query::get_loop_object_type( $looping_query_id ),
				];
			}

			// Unique identifier for inline template inside query loop (@since 1.9.1)
			$generated_inline_identifier = $looping_query_id ? implode( ':', $unique_loop_id ) : $template_id;

			/**
			 * Add template inline CSS, if:
			 * 1. Non-loop template that has not been added already
			 * 2. Is in-loop template index 0
			 * 2b. Cannot use index 0 as if we are in second page and using assign section hook, the styles not generated. Use $generated_inline_identifier as workaround - @since 1.9.1
			 *
			 * @since 1.8.2
			 */
			if (
				( ! Query::is_looping() && ! in_array( $template_id, Assets::$page_settings_post_ids ) ) ||
				( $looping_query_id && ! in_array( $generated_inline_identifier, self::$generated_inline_identifier ) )
			) {
				$inline_css .= "\n/* TEMPLATE SHORTCODE CSS (ID: {$template_id}) */\n";
				$inline_css .= $template_inline_css;
				// Add generated inline identifier to avoid duplicate inline CSS (@since 1.9.1)
				self::$generated_inline_identifier[] = $generated_inline_identifier;
			}
		}

		// Add page settings of this template
		if ( ! in_array( $template_id, Assets::$page_settings_post_ids ) ) {
			Assets::$page_settings_post_ids[] = $template_id;
		}

		/**
		 * Builder: Generate global classes & page settings CSS of Template element
		 *
		 * Frontend: Global classes in template added in wp_footer via enqueue_footer_inline_css
		 *
		 * @since 1.8.2
		 */

		// Moved to enqueue_footer_inline_css in frontend.php (@since 1.9.8)
		// $global_classes_css = Assets::generate_global_classes();

		if ( bricks_is_builder_call() ) {
			$page_css = Assets::generate_inline_css_page_settings();

			if ( $page_css ) {
				$inline_css .= "\n/* PAGE CSS */\n" . $page_css;
			}
		}

		// Webfonts
		Assets::load_webfonts( $inline_css );

		return $inline_css;
	}

	/**
	 * Keep the timestamp of the latest change in the templates post type to force the cache flush
	 *
	 * @param int $post_id Post ID.
	 */
	public function flush_templates_cache( $post_id ) {
		if ( get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG ) {
			wp_cache_set( 'last_changed', microtime(), 'bricks_' . BRICKS_DB_TEMPLATE_SLUG );
		}
	}

	/**
	 * Check if remote site can get templates
	 *
	 * @see Api::get_templates()
	 * @return array Array with 'error' key on error. Array with 'site', 'password', 'licenseKey' on success.
	 *
	 * @since 1.0
	 */
	public static function can_get_templates( $parameters ) {
		// STEP: Admin setting 'myTemplatesAccess' blocked
		if ( ! Database::get_setting( 'myTemplatesAccess' ) ) {
			return [
				'error' => [
					'code'    => 'my_templates_access_disabled',
					'message' => esc_html__( 'The site you are requesting templates from has access to their templates disabled.', 'bricks' ),
				],
			];
		}

		$site_url = ! empty( $parameters['site'] ) ? esc_url( $parameters['site'] ) : false;

		// STEP: Check 'site' provided (mandatory)
		if ( ! $site_url ) {
			return [
				'error' => [
					'code'    => 'no_site_url',
					'message' => esc_html__( 'Sorry, but no site URL has been provided.', 'bricks' ),
				],
			];
		}

		// STEP: Admin setting 'myTemplatesWhitelist' lists requesting 'site'
		$my_templates_whitelist_urls = Database::get_setting( 'myTemplatesWhitelist', [] );

		if ( $my_templates_whitelist_urls ) {
			$my_templates_whitelist_urls = array_map( 'trim', explode( "\n", $my_templates_whitelist_urls ) );

			$my_templates_whitelist_urls = array_map( 'trailingslashit', $my_templates_whitelist_urls );

			$site_url = trailingslashit( $site_url );

			if ( ! in_array( $site_url, $my_templates_whitelist_urls ) ) {
				return [
					'error' => [
						'code'    => 'not_whitelisted',
						// translators: %1$s: site URL, %2$s: current site URL
						'message' => sprintf( esc_html__( 'Your website (%1$s) has no permission to access templates from %2$s', 'bricks' ), $site_url, get_site_url() ),
					],
				];
			}
		}

		// STEP: Admin setting 'myTemplatesPassword'
		$my_templates_password = Database::get_setting( 'myTemplatesPassword' );
		$password              = isset( $parameters['password'] ) ? sanitize_text_field( $parameters['password'] ) : false;

		if ( $my_templates_password ) {
			if ( ! $password ) {
				return [
					'error' => [
						'code'    => 'remote_templates_password_required',
						'message' => esc_html__( 'The site you are requesting templates from requires a remote templates password.', 'bricks' ),
					],
				];
			}

			if ( $password !== $my_templates_password ) {
				return [
					'error' => [
						'code'    => 'remote_templates_password_incorrect',
						'message' => esc_html__( 'Your remote templates password is incorrect.', 'bricks' ),
					],
				];
			}
		}

		// STEP: ALl checks pass

		// Pass 'site' for 'bricks/get_templates' filter check
		$templates_args = [ 'site' => $site_url ];

		// Pass license key if provided
		if ( isset( $parameters['licenseKey'] ) ) {
			$templates_args['licenseKey'] = sanitize_text_field( $parameters['licenseKey'] );
		}

		// Pass templates password if provided
		if ( isset( $password ) ) {
			$templates_args['password'] = $password;
		}

		// Success: Return template_args
		return $templates_args;
	}

	/**
	 * Create template
	 *
	 * @since 1.0
	 */
	public static function get_remote_template_settings() {
		$all_remote_templates = [];

		// Get community templates
		$all_remote_templates[] = [ 'url' => BRICKS_REMOTE_URL ];

		// Get single remote template (Bricks > Settings > Templates) @pre 1.9.4
		$single_remote_template_url      = Database::get_setting( 'remoteTemplatesUrl' );
		$single_remote_template_password = Database::get_setting( 'remoteTemplatesPassword' );

		if ( $single_remote_template_url ) {
			$all_remote_templates[] = [
				'url'      => $single_remote_template_url,
				'password' => $single_remote_template_password,
			];
		}

		// Get remote templates (Bricks > Settings > Templates) @since 1.9.4
		$remote_templates = Database::get_setting( 'remoteTemplates' ) ?? false;

		if ( is_array( $remote_templates ) ) {
			// Append remote templates to all remote templates
			$all_remote_templates = array_merge( $all_remote_templates, $remote_templates );
		}

		return $all_remote_templates;
	}

	/**
	 * Builder templates: Get all remote templates data (templates, authors, bundles, tags)
	 *
	 * @return array
	 *
	 * @since 1.0
	 */
	public static function get_remote_templates_data() {
		$source                   = $_POST['source'] ?? '';
		$remote_template_settings = self::get_remote_template_settings();
		$remote_template_url      = '';
		$remote_template_password = '';

		// Get remote template 'url' and 'password'
		foreach ( $remote_template_settings as $template_settings ) {
			if ( isset( $template_settings['url'] ) && $template_settings['url'] === $source ) {
				$remote_template_url      = $template_settings['url'];
				$remote_template_password = $template_settings['password'] ?? '';
			}
		}

		$request_url = Api::get_endpoint( 'get-templates-data', $source );
		$request_url = add_query_arg( [ 'site' => get_site_url() ], $request_url );

		if ( $remote_template_password ) {
			$request_url = add_query_arg( [ 'password' => urlencode( $remote_template_password ) ], $request_url );
		}

		// Community templates: Send license key
		// TODO NOTE: Currently not being checked on Bricks community templates site
		if ( $source == BRICKS_REMOTE_URL ) {
			$request_url = add_query_arg( [ 'licenseKey' => License::$license_key ], $request_url );
		}

		$request_url = add_query_arg( [ 'time' => time() ], $request_url );

		if ( strpos( $request_url, 'bricksbuilder.io/wp-json' ) !== false ) {
			$request_url = str_replace( 'bricksbuilder.io/wp-json', 'bricksbuilder.io/api/', $request_url );
		}

		$response = Helpers::remote_get( $request_url );

		// Return error to show in builder templates manager
		if ( is_wp_error( $response ) ) {
			return [
				'error'       => $response->get_error_message(),
				'request_url' => $request_url,
			];
		}

		$remote_templates = json_decode( wp_remote_retrieve_body( $response ), true );
		$remote_templates = apply_filters( 'bricks/get_remote_templates_data', $remote_templates );

		if ( ! empty( $remote_templates['error']['message'] ) ) {
			return [
				'error'       => $remote_templates['error']['message'],
				'request_url' => $request_url,
			];
		}

		return $remote_templates;
	}

	/**
	 * Get templates query based on custom args
	 *
	 * @since 1.0
	 *
	 * @param array $custom_args
	 * @return WP_Query
	 */
	public static function get_templates_query( $custom_args = [] ) {
		$last_changed = wp_cache_get_last_changed( 'bricks_' . BRICKS_DB_TEMPLATE_SLUG );
		$cache_key    = md5( 'get_templates_query_' . $last_changed . wp_json_encode( $custom_args ) );

		// Undocumented (@since 1.9.9)
		$cache_key = apply_filters( 'bricks/get_templates_query/cache_key', $cache_key );

		$query = wp_cache_get( $cache_key, 'bricks' );

		if ( $query === false ) {
			$default_args = [
				'post_type'      => BRICKS_DB_TEMPLATE_SLUG,
				'posts_per_page' => -1,
				'post_status'    => 'publish',
			];

			// Undocumented (@since 1.9.9)
			$merged_args = apply_filters( 'bricks/get_templates/query_vars', wp_parse_args( $custom_args, $default_args ) );

			$query = new \WP_Query( $merged_args );

			wp_cache_set( $cache_key, $query, 'bricks', DAY_IN_SECONDS );
		}

		return $query;
	}

	/**
	 * Get all the template IDs of a specific type
	 */
	public static function get_templates_by_type( $template_type = '' ) {
		$query_args = [
			'meta_query' => [
				[
					'key'   => BRICKS_DB_TEMPLATE_TYPE,
					'value' => $template_type,
				],
			],
			'fields'     => 'ids',
		];

		$query = self::get_templates_query( $query_args );

		return ! empty( $query->found_posts ) ? $query->posts : [];
	}

	/**
	 * Get my templates
	 *
	 * @since 1.0
	 */
	public static function get_templates( $custom_args = [] ) {
		$templates_query = self::get_templates_query( $custom_args );

		$templates = [];

		if ( $templates_query->have_posts() ) {
			foreach ( $templates_query->get_posts() as $template ) {
				// Template bundles
				$template_bundles = wp_get_object_terms( $template->ID, BRICKS_DB_TEMPLATE_TAX_BUNDLE, [ 'fields' => 'slugs' ] );
				$bundles          = [];

				if ( $template_bundles ) {
					foreach ( $template_bundles as $bundle ) {
						$bundles[] = $bundle;
					}
				}

				// Template tags
				$template_tags = wp_get_object_terms( $template->ID, BRICKS_DB_TEMPLATE_TAX_TAG, [ 'fields' => 'slugs' ] );
				$tags          = [];

				if ( $template_tags ) {
					foreach ( $template_tags as $tag ) {
						$tags[] = $tag;
					}
				}

				$author_name = get_the_author_meta( 'display_name', $template->post_author );

				// Check if my template thumbnail exists locally in WP root 'template-screenshots' folder
				$template_thumbnail_path = ABSPATH . trailingslashit( 'template-screenshots' ) . $template->post_name . '.jpg';

				if ( file_exists( $template_thumbnail_path ) ) {
					$template_thumbnail = get_site_url( null, '/' ) . trailingslashit( 'template-screenshots' ) . $template->post_name . '.jpg';
				}

				// Fallback: Check template featured image
				else {
					$template_thumbnail = has_post_thumbnail( $template->ID ) ? get_the_post_thumbnail_url( $template->ID, 'bricks_medium' ) : false;
				}

				$template_data = [
					'id'             => $template->ID,
					'name'           => $template->post_name,
					'title'          => $template->post_title,
					'date'           => $template->post_date,
					'date_formatted' => date( get_option( 'date_format' ), strtotime( $template->post_date ) ),
					'author'         => [
						'name'   => $author_name,
						'avatar' => get_avatar_url( $template->post_author, [ 'size' => 60 ] ),
						'url'    => get_the_author_meta( 'user_url', $template->post_author ),
					],
					'permalink'      => get_permalink( $template->ID ),
					'thumbnail'      => $template_thumbnail,
					'bundles'        => $bundles,
					'tags'           => $tags,
					'type'           => self::get_template_type( $template->ID ),
				];

				$template_elements = [];
				$area              = false;

				if ( is_array( get_post_meta( $template->ID, BRICKS_DB_PAGE_HEADER, true ) ) ) {
					$template_elements       = get_post_meta( $template->ID, BRICKS_DB_PAGE_HEADER, true );
					$template_data['header'] = $template_elements;
					$area                    = 'header';
				}

				if ( is_array( get_post_meta( $template->ID, BRICKS_DB_PAGE_CONTENT, true ) ) ) {
					$template_elements        = get_post_meta( $template->ID, BRICKS_DB_PAGE_CONTENT, true );
					$template_data['content'] = $template_elements;
					$area                     = 'content';
				}

				if ( is_array( get_post_meta( $template->ID, BRICKS_DB_PAGE_FOOTER, true ) ) ) {
					$template_elements       = get_post_meta( $template->ID, BRICKS_DB_PAGE_FOOTER, true );
					$template_data['footer'] = $template_elements;
					$area                    = 'footer';
				}

				// Remove 'signature' from remote template element settings
				if ( $area && ! isset( $custom_args['remove_code_signature'] ) ) {
					foreach ( $template_elements as $index => $template_element ) {
						if ( ! empty( $template_element['name'] ) && in_array( $template_element['name'], [ 'code', 'svg' ] ) && isset( $template_element['settings']['signature'] ) ) {
							unset( $template_elements[ $index ]['settings']['signature'] );
						}

						if ( isset( $template_element['settings']['query']['signature'] ) ) {
							unset( $template_elements[ $index ]['settings']['query']['signature'] );
						}
					}

					$template_data[ $area ] = $template_elements;
				}

				$template_page_settings = get_post_meta( $template->ID, BRICKS_DB_PAGE_SETTINGS, true );

				if ( $template_page_settings ) {
					$template_data['pageSettings'] = $template_page_settings;
				}

				$templates[] = $template_data;
			}
		}

		// Filter templates
		$templates = apply_filters( 'bricks/get_templates', $templates, $custom_args );

		return $templates;
	}

	/**
	 * Get template authors
	 *
	 * @since 1.0
	 *
	 * @return array
	 */
	public static function get_template_authors() {
		$template_ids = get_posts(
			[
				'post_type'      => BRICKS_DB_TEMPLATE_SLUG,
				'posts_per_page' => -1,
				'fields'         => 'ids',
			]
		);

		$template_authors = [];

		foreach ( $template_ids as $template_id ) {
			$template_author_id = get_post_field( 'post_author', $template_id );
			$template_author    = get_the_author_meta( 'display_name', $template_author_id );

			if ( ! in_array( $template_author, $template_authors ) ) {
				$template_authors[] = $template_author;
			}
		}

		// Filter template authors
		$template_authors = apply_filters( 'bricks/get_template_authors', $template_authors );

		return $template_authors;
	}

	/**
	 * Get template bundles
	 *
	 * @since 1.0
	 */
	public static function get_template_bundles() {
		$terms = get_terms(
			[
				'taxonomy' => BRICKS_DB_TEMPLATE_TAX_BUNDLE,
			]
		);

		if ( ! is_array( $terms ) ) {
			return false;
		}

		$template_bundles = [];

		foreach ( $terms as $term ) {
			$term_obj                        = get_term( $term );
			$template_bundles[ $term->slug ] = $term->name;
		}

		// Filter template bundles
		$template_bundles = apply_filters( 'bricks/get_template_bundles', $template_bundles );

		return $template_bundles;
	}

	/**
	 * Get template tags
	 *
	 * @since 1.0
	 */
	public static function get_template_tags() {
		$terms = get_terms( [ 'taxonomy' => BRICKS_DB_TEMPLATE_TAX_TAG ] );

		if ( ! is_array( $terms ) ) {
			return false;
		}

		$template_tags = [];

		foreach ( $terms as $term ) {
			$term_obj                     = get_term( $term );
			$template_tags[ $term->slug ] = $term->name;
		}

		// Filter template bundles
		$template_tags = apply_filters( 'bricks/get_template_tags', $template_tags );

		return $template_tags;
	}

	/**
	 * Get template type via post_meta
	 *
	 * @param int $post_id
	 *
	 * @since 1.0
	 */
	public static function get_template_type( $post_id = 0 ) {
		if ( ! $post_id ) {
			$post_id = get_the_ID();
		}

		$template_type = get_post_meta( $post_id, BRICKS_DB_TEMPLATE_TYPE, true );

		if ( isset( $template_type ) ) {
			return $template_type;
		}

		// Fallback: Check for content type if no template type post meta found
		if ( get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG ) {
			// Check for header template
			$header_template = get_post_meta( $post_id, BRICKS_DB_PAGE_HEADER, true );

			if ( is_array( $header_template ) ) {
				return 'header';
			}

			// Check for content template
			$content_template = get_post_meta( $post_id, BRICKS_DB_PAGE_CONTENT, true );

			if ( is_array( $content_template ) ) {
				return 'content';
			}

			// Check for footer template
			$footer_template = get_post_meta( $post_id, BRICKS_DB_PAGE_FOOTER, true );

			if ( is_array( $footer_template ) ) {
				return 'footer';
			}
		} else {
			// Post type other than bricks_template
			return 'content';
		}

		return;
	}

	/**
	 * Get template by ID
	 *
	 * @since 1.0
	 */
	public static function get_template_by_id( $template_id ) {
		$template = self::get_templates(
			[
				'p'           => $template_id,
				'post_status' => 'any', // @since 1.5.1
			]
		);

		// Check if template match found
		if ( count( $template ) === 1 ) {
			$template = $template[0];
		} else {
			$template = false;
		}

		return $template;
	}

	/**
	 * Builder: Create template
	 *
	 * @since 1.0
	 */
	public function create_template() {
		Ajax::verify_request( 'bricks-nonce-builder' );

		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		$template_data = $_POST['templateData'] ?? [];

		// Insert new template into db
		$insert_post_data = [
			'post_status' => current_user_can( 'publish_posts' ) ? 'publish' : 'pending',
			'post_title'  => ! empty( $template_data['templateTitle'] ) ? esc_html( $template_data['templateTitle'] ) : esc_html__( '(no title)', 'bricks' ),
			'post_type'   => BRICKS_DB_TEMPLATE_SLUG,
		];

		$insert_post_data['tax_input'] = [];

		// Save template bundle term
		if ( isset( $template_data['templateBundle'] ) ) {
			$insert_post_data['tax_input'][ BRICKS_DB_TEMPLATE_TAX_BUNDLE ] = $template_data['templateBundle'];
		}

		// Save template tags
		if ( isset( $template_data['templateTags'] ) ) {
			$insert_post_data['tax_input'][ BRICKS_DB_TEMPLATE_TAX_TAG ] = $template_data['templateTags'];
		}

		$template_id = wp_insert_post( $insert_post_data );

		// Save template type in post meta
		if ( isset( $template_data['templateType'] ) ) {
			update_post_meta(
				$template_id,
				BRICKS_DB_TEMPLATE_TYPE,
				$template_data['templateType']
			);
		}

		$my_templates = self::get_templates(
			[
				'post_status' => 'any',
			]
		);

		wp_send_json_success( $my_templates );
	}

	/**
	 * Builder: Save template
	 *
	 * @since 1.0
	 */
	public function save_template() {
		Ajax::verify_request( 'bricks-nonce-builder' );

		$template_data = Ajax::decode( $_POST['templateData'] ?? [] );

		// Insert new template into database
		$insert_post_data = [
			'post_status' => current_user_can( 'publish_posts' ) ? 'publish' : 'pending',
			'post_title'  => $template_data['templateTitle'] ?? esc_html__( '(no title)', 'bricks' ),
			'post_type'   => BRICKS_DB_TEMPLATE_SLUG,
		];

		$insert_post_data['tax_input'] = [];

		// Save template bundle term
		if ( isset( $template_data['templateBundle'] ) ) {
			$insert_post_data['tax_input'][ BRICKS_DB_TEMPLATE_TAX_BUNDLE ] = $template_data['templateBundle'];
		}

		// Save template tags
		if ( isset( $template_data['templateTags'] ) ) {
			$insert_post_data['tax_input'][ BRICKS_DB_TEMPLATE_TAX_TAG ] = $template_data['templateTags'];
		}

		$template_id = wp_insert_post( $insert_post_data );

		switch ( $template_data['templateType'] ) {
			case 'header':
				$meta_key          = BRICKS_DB_PAGE_HEADER;
				$template_elements = $template_data['header'];
				break;

			case 'footer':
				$meta_key          = BRICKS_DB_PAGE_FOOTER;
				$template_elements = $template_data['footer'];
				break;

			default:
				$meta_key          = BRICKS_DB_PAGE_CONTENT;
				$template_elements = $template_data['content'];
				break;
		}

		// STEP: Generate element IDs (@since 1.9.8)
		$template_elements = Helpers::generate_new_element_ids( $template_elements );

		// Save data
		update_post_meta( $template_id, $meta_key, $template_elements );

		// Save template type
		update_post_meta( $template_id, BRICKS_DB_TEMPLATE_TYPE, $template_data['templateType'] );

		// Fetch all templates
		$my_templates = self::get_templates(
			[
				'post_status' => 'any',
			]
		);

		wp_send_json_success(
			[
				'templateId'  => $template_id,
				'myTemplates' => $my_templates,
			]
		);
	}

	/**
	 * Builder: Move template to trash
	 *
	 * @since 1.0
	 */
	public function delete_template() {
		Ajax::verify_request( 'bricks-nonce-builder' );

		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		$template_id = ! empty( $_POST['templateId'] ) ? intval( $_POST['templateId'] ) : false;

		// Double-check if user is allowed to delete template
		if ( ! Capabilities::current_user_can_use_builder( $template_id ) ) {
			$my_templates = self::get_templates(
				[
					'post_status' => 'any',
				]
			);

			wp_send_json_success( $my_templates );
		}

		if ( $template_id ) {
			wp_trash_post( $template_id );
		}

		$my_templates = self::get_templates( [ 'post_status' => 'any' ] );

		wp_send_json_success( $my_templates );
	}

	/**
	 * Admin & builder: Import template
	 *
	 * @since 1.0
	 */
	public function import_template() {
		if ( isset( $_POST['builder'] ) ) {
			Ajax::verify_nonce( 'bricks-nonce-builder' );
		} else {
			Ajax::verify_nonce( 'bricks-nonce-admin' );
		}

		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		/**
		 * Builder: Get global classes via 'globalClasses'
		 *
		 * @since 1.7.1 - json_decode instead of Ajax::decode to not run wp_slash
		 */
		$global_classes = ! empty( $_POST['globalClasses'] ) ? json_decode( $_POST['globalClasses'] ) : false;

		// Fallback: Get global classes from database
		if ( ! is_array( $global_classes ) || ( is_array( $global_classes ) && ! count( $global_classes ) ) ) {
			$global_classes = get_option( BRICKS_DB_GLOBAL_CLASSES, [] );
		}

		// Load WP_WP_Filesystem for temp file URL access
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';

			WP_Filesystem();
		}

		$templates = [];

		$is_zip_file = isset( $_FILES['files']['name'][0] ) ? pathinfo( $_FILES['files']['name'][0], PATHINFO_EXTENSION ) === 'zip' : false;

		if ( $is_zip_file ) {
			// Check if ZipArchive PHP extension exists
			if ( ! class_exists( '\ZipArchive' ) ) {
				wp_send_json_error( [ 'error' => 'Error: ZipArchive PHP extension does not exist.' ] );
			}

			$zip = new \ZipArchive();

			$wp_upload_dir = wp_upload_dir();

			$temp_path = trailingslashit( $wp_upload_dir['basedir'] ) . BRICKS_TEMP_DIR;

			// Create temp path if it doesn't exist
			wp_mkdir_p( $temp_path );

			if ( isset( $_FILES['files']['tmp_name'][0] ) ) {
				$zip->open( $_FILES['files']['tmp_name'][0] );
			}

			// Extract JSON files to temp directory
			$zip->extractTo( $temp_path );

			$zip->close();

			// Get all extracted JSON files (exclude '.' system files and reset array with array_values)
			$file_names = array_values( preg_grep( '/^([^.])/', scandir( $temp_path ) ) );

			foreach ( $file_names as $file_name ) {
				$templates[] = json_decode( $wp_filesystem->get_contents( trailingslashit( $temp_path ) . $file_name ), true );

				// Remove JSON file
				unlink( trailingslashit( $temp_path ) . $file_name );
			}

			// Remove temp directory
			rmdir( $temp_path );
		} else {
			// Import single JSON file
			$files = $_FILES['files']['tmp_name'] ?? [];

			foreach ( $files as $file ) {
				$templates[] = json_decode( $wp_filesystem->get_contents( $file ), true );
			}
		}

		foreach ( $templates as $template_data ) {
			$insert_post_data = [
				'post_status' => current_user_can( 'publish_posts' ) ? 'publish' : 'pending',
				'post_title'  => ! empty( $template_data['title'] ) ? $template_data['title'] : esc_html__( '(no title)', 'bricks' ),
				'post_type'   => BRICKS_DB_TEMPLATE_SLUG,
			];

			// Template tags (terms)
			if ( is_array( $template_data['tags'] ) ) {
				if ( count( $template_data['tags'] ) ) {
					$insert_post_data['tax_input'] = [ BRICKS_DB_TEMPLATE_TAX_TAG => $template_data['tags'] ];
				}
			}

			// Template bundles (terms)
			if ( is_array( $template_data['bundles'] ) ) {
				if ( count( $template_data['bundles'] ) ) {
					$insert_post_data['tax_input'] = [ BRICKS_DB_TEMPLATE_TAX_BUNDLE => $template_data['bundles'] ];
				}
			}

			$new_template_id = wp_insert_post( $insert_post_data );
			$area            = 'content';
			$meta_key        = BRICKS_DB_PAGE_CONTENT;
			$elements        = false;

			if ( ! empty( $template_data['templateType'] ) ) {
				update_post_meta( $new_template_id, BRICKS_DB_TEMPLATE_TYPE, $template_data['templateType'] );
			}

			if ( ! empty( $template_data['header'] ) ) {
				$area     = 'header';
				$meta_key = BRICKS_DB_PAGE_HEADER;
			} elseif ( ! empty( $template_data['footer'] ) ) {
				$area     = 'footer';
				$meta_key = BRICKS_DB_PAGE_FOOTER;
			}

			if ( ! empty( $template_data[ $area ] ) ) {
				$elements = $template_data[ $area ];
			}

			if ( isset( $template_data['pageSettings'] ) ) {
				update_post_meta( $new_template_id, BRICKS_DB_PAGE_SETTINGS, $template_data['pageSettings'] );
			}

			// Add template settings (@since 1.8.1)
			if ( isset( $template_data['templateSettings'] ) ) {
				Helpers::set_template_settings( $new_template_id, $template_data['templateSettings'] );
			}

			// STEP: Add global classes used in template to global classes in this database
			$template_global_classes         = ! empty( $template_data['global_classes'] ) ? $template_data['global_classes'] : [];
			$map_classes                     = []; // @see PopupTemplates.vue (@since 1.5.1)
			$maybe_pseudo_class_setting_keys = [];

			foreach ( $template_global_classes as $template_class ) {
				// STEP: Add template setting keys to create missing pseudo class from (@since 1.7.1)
				if ( ! empty( $template_class['settings'] ) ) {
					$maybe_pseudo_class_setting_keys = array_merge( $maybe_pseudo_class_setting_keys, array_keys( $template_class['settings'] ) );
				}

				// Skip: Class with same unique 'id' exists locally
				$class_index = array_search( $template_class['id'], array_column( $global_classes, 'id' ) );

				if ( $class_index !== false ) {
					continue;
				}

				// Add to map_classes, then skip (global class with this 'name' already exists in this installation)
				$class_index = array_search( $template_class['name'], array_column( $global_classes, 'name' ) );

				if ( $class_index !== false ) {
					$map_classes[ $template_class['id'] ] = $global_classes[ $class_index ]['id'];

					continue;
				}

				// Update global classes in database
				$global_classes[] = $template_class;
			}

			// Loop over all mapped classes to replace template element class id's with local class id's
			foreach ( $map_classes as $template_class_id => $local_class_id ) {
				foreach ( $elements as $index => $element ) {
					$element_classes = ! empty( $element['settings']['_cssGlobalClasses'] ) ? $element['settings']['_cssGlobalClasses'] : [];

					if ( count( $element_classes ) ) {
						foreach ( $element_classes as $class_index => $element_class_id ) {
							if ( $element_class_id === $template_class_id ) {
								$element_classes[ $class_index ] = $local_class_id;
							}
						}

						$elements[ $index ]['settings']['_cssGlobalClasses'] = $element_classes;
					}
				}
			}

			// STEP: Update global classes in db
			$global_classes_response = Helpers::save_global_classes_in_db( $global_classes );

			// STEP: Save final template elements
			$elements = Helpers::sanitize_bricks_data( $elements );

			// Add back slashes to element settings (needed for '_content' HTML entities, and Custom CSS) @since 1.7.1
			foreach ( $elements as $index => $element ) {
				$element_settings = ! empty( $element['settings'] ) ? $element['settings'] : [];

				foreach ( $element_settings as $setting_key => $setting_value ) {
					if ( is_string( $setting_value ) ) {
						$elements[ $index ]['settings'][ $setting_key ] = addslashes( $setting_value );
					}
				}
			}

			// STEP: Generate element IDs (@since 1.9.8)
			$elements = Helpers::generate_new_element_ids( $elements );

			update_post_meta( $new_template_id, $meta_key, $elements );

			// STEP: Generate CSS file for imported template
			if ( Database::get_setting( 'cssLoading' ) === 'file' && $elements ) {
				$template_css_file_name = Assets_Files::generate_post_css_file( $new_template_id, $area, $elements );
			}

			// STEP: Add pseudo elements & classes used in the template to the database (@since 1.7.1)

			// Get latest pseudo classes from builder
			$pseudo_classes = ! empty( $_POST['pseudoClasses'] ) ? Ajax::decode( $_POST['pseudoClasses'], false ) : [];

			// Add element setting keys to create missing pseudo class from
			foreach ( $elements as $element ) {
				if ( ! empty( $element['settings'] ) ) {
					$maybe_pseudo_class_setting_keys = array_merge( $maybe_pseudo_class_setting_keys, array_keys( $element['settings'] ) );
				}
			}

			$all_pseudo_classes = self::template_import_create_missing_pseudo_classes( $pseudo_classes, $maybe_pseudo_class_setting_keys );
			$all_pseudo_classes = array_unique( $all_pseudo_classes );

			// Update pseudo classes db entry (if we got more items than before)
			if ( count( $all_pseudo_classes ) > count( $pseudo_classes ) ) {
				update_option( BRICKS_DB_PSEUDO_CLASSES, $all_pseudo_classes );
			}
		}

		$my_templates = self::get_templates(
			[
				'post_status' => 'any',
			]
		);

		wp_send_json_success(
			[
				'my_templates'   => $my_templates,
				'global_classes' => $global_classes,
				'pseudo_classes' => $all_pseudo_classes,
			]
		);
	}

	/**
	 * STEP: Check global class setting key for occurence of pseudo element to create pseudo element in local installtion
	 *
	 * @since 1.7.1
	 */
	public static function template_import_create_missing_pseudo_classes( $pseudo_classes, $setting_keys = [] ) {
		// Pseudo elements source of truth: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
		$valid_pseudo_elements = [
			'::after',
			':after',

			'::backdrop',
			':backdrop',

			'::before',
			':before',

			'::cue',
			':cue',

			'::cue-region',
			':cue-region',

			'::first-letter',
			':first-letter',

			'::first-line',
			':first-line',

			'::file-selector-button',
			':file-selector-button',

			'::grammar-error',
			':grammar-error',

			'::marker',
			':marker',

			// '::part(',

			'::placeholder',
			':placeholder',

			'::selection',
			':selection',

			// '::slotted(',

			'::spelling-error',
			':spelling-error',

			'::target-text',
			':target-text',
		];

		// Pseudo classes source of truth: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
		$valid_pseudo_classes = [
			':active',
			':any-link',
			':autofill',
			':blank', // Experimental
			':checked',
			':current', // Experimental
			':default',
			':defined',
			':dir(', // Experimental
			':disabled',
			':empty',
			':enabled',
			':first',
			':first-child',
			':first-of-type',
			':fullscreen',
			':future', // Experimental
			':focus',
			':focus-visible',
			':focus-within',
			':has(', // Experimental
			':host',
			':host(',
			':host-context(', // Experimental
			':hover',
			':indeterminate',
			':in-range',
			':invalid',
			':is(',
			':lang(',
			':last-child',
			':last-of-type',
			':left',
			':link',
			':local-link', // Experimental
			':modal',
			':not(',
			':nth-child(',
			':nth-col(', // Experimental
			':nth-last-child(',
			':nth-last-col(', // Experimental
			':nth-last-of-type(',
			':nth-of-type(',
			':only-child',
			':only-of-type',
			':optional',
			':out-of-range',
			':past', // Experimental
			':picture-in-picture',
			':placeholder-shown',
			':paused',
			':playing',
			':read-only',
			':read-write',
			':required',
			':right',
			':root',
			':scope',
			':state(', // Experimental
			':target',
			':target-within', // Experimental
			':user-invalid', // Experimental
			':valid',
			':visited',
			':where(',
		];

		// Loop over all settings keys to find pseudo classes & pseudo elements
		foreach ( $setting_keys as $setting_key ) {
			// Pseudo class is always the last setting key part
			$setting_key_parts  = explode( ':', $setting_key );
			$maybe_pseudo_class = ':' . end( $setting_key_parts );

			// STEP: Detect pseudo classes
			foreach ( $valid_pseudo_classes as $pseudo_class ) {
				// Pseudo class with arguments: :nth-child(even)
				if ( strpos( $pseudo_class, '(' ) !== false ) {
					if (
						strpos( $maybe_pseudo_class, $pseudo_class ) !== false &&
						! in_array( $maybe_pseudo_class, $pseudo_classes )
					) {
						$pseudo_classes[] = $maybe_pseudo_class;
						break;
					}
				}

				// All other pseudo classes
				elseif (
					substr( $setting_key, -strlen( $pseudo_class ) ) === $pseudo_class && // setting key ends with pseudo clas
					substr( $setting_key, strpos( $setting_key, $pseudo_class ) - 1, 1 ) !== ':' && // charcter before pseudo clas is not a ':'
					! in_array( $pseudo_class, $pseudo_classes ) // pseudo class not part of global pseudo classes array
				) {
					$pseudo_classes[] = $pseudo_class;
					break;
				}
			}

			// STEP: Detect pseudo elements
			foreach ( $valid_pseudo_elements as $pseudo_element ) {
				if (
					substr( $setting_key, -strlen( $pseudo_element ) ) === $pseudo_element && // setting key ends with pseudo element
					substr( $setting_key, strpos( $setting_key, $pseudo_element ) - 1, 1 ) !== ':' && // charcter before pseudo element is not a ':'
					! in_array( $pseudo_element, $pseudo_classes ) // pseudo element not part of global pseudo classes array
				) {
					$pseudo_classes[] = $pseudo_element;
					break;
				}
			}
		}

		return $pseudo_classes;
	}

	/**
	 * Export template as JSON file
	 *
	 * @param int $template_id Provided if bulk action export.
	 * @see: admin.php:export_templates()
	 * @since 1.0
	 *
	 * @return array
	 */
	public static function export_template( $template_id = 0 ) {
		// No template_id passed: Admin or builder call
		if ( ! $template_id ) {
			if ( isset( $_GET['builder'] ) ) {
				Ajax::verify_nonce( 'bricks-nonce-builder' );
			} else {
				Ajax::verify_nonce( 'bricks-nonce-admin' );
			}

			$template_id = isset( $_GET['templateId'] ) ? intval( $_GET['templateId'] ) : 0;
		}

		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		if ( ! $template_id ) {
			wp_send_json_error( 'export_template:error: no templateId provided' );
		}

		$template_data = self::get_template_by_id( $template_id );

		$template_type = get_post_meta( $template_id, BRICKS_DB_TEMPLATE_TYPE, true );

		$template_data['templateType'] = $template_type;

		/**
		 * STEP: Add global CSS classes used in template to template data (so it can later be imported as well)
		 *
		 * @since 1.4
		 */
		if ( $template_type === 'header' || $template_type === 'footer' ) {
			$template_elements = isset( $template_data[ $template_type ] ) ? $template_data[ $template_type ] : [];
		} else {
			$template_elements = isset( $template_data['content'] ) ? $template_data['content'] : [];
		}

		/**
		 * STEP: Add template settings to template data
		 *
		 * NOTE: Should we remove 'templatePreview...' settings too?
		 *
		 * @since 1.8.1
		 */
		$template_settings = Helpers::get_template_settings( $template_id );

		// Remove template conditions
		if ( isset( $template_settings['templateConditions'] ) ) {
			unset( $template_settings['templateConditions'] );
		}

		// Save as templateSettings, to be imported later
		if ( is_array( $template_settings ) && ! empty( $template_settings ) ) {
			$template_data['templateSettings'] = $template_settings;
		}

		$template_classes = [];

		foreach ( $template_elements as $element ) {
			if ( ! empty( $element['settings']['_cssGlobalClasses'] ) ) {
				$template_classes = array_unique( array_merge( $template_classes, $element['settings']['_cssGlobalClasses'] ) );
			}
		}

		// Add class definition to template data
		$global_classes        = get_option( BRICKS_DB_GLOBAL_CLASSES, [] );
		$global_classes_to_add = [];

		foreach ( $global_classes as $global_class ) {
			if ( in_array( $global_class['id'], $template_classes ) ) {
				$global_classes_to_add[] = $global_class;
			}
		}

		if ( count( $global_classes_to_add ) ) {
			$template_data['global_classes'] = $global_classes_to_add;
		}

		// Add all global variables to template data (@since 1.9.8)
		$global_variables = ! Database::get_setting( 'disableVariablesManager', false ) ? get_option( BRICKS_DB_GLOBAL_VARIABLES, [] ) : [];
		if ( count( $global_variables ) ) {
			$template_data['globalVariables'] = $global_variables;
		}

		// Add all global variables categories to template data (@since 1.9.8)
		$global_variables_categories = ! Database::get_setting( 'disableVariablesManager', false ) ? get_option( BRICKS_DB_GLOBAL_VARIABLES_CATEGORIES, [] ) : [];
		if ( count( $global_variables_categories ) ) {
			$template_data['globalVariablesCategories'] = $global_variables_categories;
		}

		// Lowercase
		$file_name = ! empty( $template_data['title'] ) ? strtolower( $template_data['title'] ) : 'no-title';

		// Make alphanumeric (removes all other characters)
		$file_name = preg_replace( '/[^a-z0-9_\s-]/', '', $file_name );

		// Clean up multiple dashes or whitespaces
		$file_name = preg_replace( '/[\s-]+/', ' ', $file_name );

		// Convert whitespaces and underscore to dashes
		$file_name = preg_replace( '/[\s_]/', '-', $file_name );

		// Final file name
		$file_name = 'template-' . $file_name . '-' . date( 'Y-m-d' ) . '.json';

		if ( bricks_is_builder_call() ) {
			// Download individual template
			header( 'Content-Type:application/json; charset=utf-8' );
			header( "Content-Disposition: attachment; filename=$file_name" );
			header( 'Content-Transfer-Encoding: binary' );
			header( 'Cache-Control: must-revalidate' );
			header( 'Expires: 0' );
			header( 'Pragma: public' );

			// Disable zlib compression to avoid empty template (#31nepuc @since 1.6)
			ini_set( 'zlib.output_compression', '0' );

			@ob_end_flush();

			echo wp_json_encode( $template_data );
			die;
		} else {
			// Bulk action: Export
			return [
				'name'    => $file_name,
				'content' => wp_json_encode( $template_data ),
			];
		}
	}

	/**
	 * Check if setting value has image/svg properties
	 *
	 * @since 1.3.2
	 */
	public static function is_image( $setting ) {
		return isset( $setting['id'] ) && isset( $setting['url'] ) &&
			( isset( $setting['size'] ) && isset( $setting['full'] ) ) ||
			( isset( $setting['url'] ) && strpos( $setting['url'], '.svg' ) !== false );
	}

	/**
	 * Recursive function: Import remote element images from template data
	 *
	 * @since 1.3.2
	 */
	public static function import_images( $settings, $import_images ) {
		foreach ( $settings as $key => $value ) {
			if ( self::is_image( $value ) ) {
				self::import_image( $value, $import_images );
			} elseif ( is_array( $value ) ) {
				self::import_images( $value, $import_images );
			}
		}
	}

	public static function import_image( $image, $import_images ) {
		if ( ! $image ) {
			return [ 'error' => 'No image provided.' ];
		}

		if ( ! isset( $image['url'] ) ) {
			return [ 'error' => 'No image URL provided.' ];
		}

		// Check if SVG (SVG has no 'full' and 'size' attributes)
		$is_svg = pathinfo( $image['url'], PATHINFO_EXTENSION ) === 'svg';

		// STEP: No image import requested: Return placeholder image
		if ( ! $import_images && ! $is_svg ) {
			// Add to instance property to replace templateData before returning it to Vue
			$placeholder_image = [
				'url'  => Builder::get_template_placeholder_image(),
				'full' => Builder::get_template_placeholder_image(),
			];

			self::$template_images[] = [
				'old' => $image,
				'new' => $placeholder_image,
			];

			return $placeholder_image;
		}

		// Not allowed to upload SVG: Remove 'file' value
		elseif ( $import_images && $is_svg && ! Capabilities::current_user_can_upload_svg() && ! empty( $image['url'] ) ) {
			$svg_blank = $image;
			unset( $svg_blank['url'] );

			if ( isset( $svg_blank['filename'] ) ) {
				unset( $svg_blank['filename'] );
			}

			self::$template_images[] = [
				'old' => $image,
				'new' => $svg_blank,
			];
		}

		if ( ! isset( $image['id'] ) ) {
			return [ 'error' => 'No image ID provided (i.e. it is a placeholder image).' ];
		}

		if ( ! $is_svg && ! isset( $image['full'] ) ) {
			return [ 'error' => 'No full URL provided.' ];
		}

		if ( ! $is_svg && ! isset( $image['size'] ) ) {
			return [ 'error' => 'No image size provided.' ];
		}

		$image_size     = $is_svg ? 'full' : $image['size'];
		$image_full_url = $is_svg ? $image['url'] : $image['full'];
		$filename       = basename( $image_full_url );

		// Check if image has been downloaded before (by post meta '_bricks_image_origin_url' against image 'full' URL)
		global $wpdb;

		// Return existing image ID if match found in db
		$existing_image_id = $wpdb->get_var( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_bricks_image_origin_url' AND meta_value = '$image_full_url'" );

		// Check for existing image ID by URL
		if ( ! $existing_image_id ) {
			$existing_image_id = attachment_url_to_postid( $image_full_url );
		}

		if ( $existing_image_id ) {
			$new_image_full_url = wp_get_attachment_image_url( $existing_image_id, 'full' );
		}

		// Return existing image as an object
		if ( $existing_image_id && $new_image_full_url ) {
			$existing_image = [
				'id'       => $existing_image_id,
				'filename' => $filename,
				'size'     => $image_size,
				'full'     => $new_image_full_url,
				'url'      => wp_get_attachment_image_url( $existing_image_id, $image_size ),
			];

			// Add to instance property to replace templateData before returning it to Vue
			self::$template_images[] = [
				'old' => $image,
				'new' => $existing_image,
			];

			return $existing_image;
		}

		// Image not found in db: Download new image, then return new image object
		$remote_image = Helpers::remote_get( $image_full_url );

		$type = wp_remote_retrieve_header( $remote_image, 'content-type' );

		$mirror = wp_upload_bits( $filename, null, wp_remote_retrieve_body( $remote_image ) );

		$new_attachment = [
			'post_title'     => $filename,
			'post_mime_type' => $type,
		];

		if ( ! isset( $mirror['file'] ) ) {
			return [
				'error'  => 'Error: wp_upload_bits failed (no "file" passed)',
				'mirror' => $mirror,
			];
		}

		$new_attachment_id = wp_insert_attachment( $new_attachment, $mirror['file'] );

		require_once ABSPATH . 'wp-admin/includes/image.php';

		$new_attachment_metadata = wp_generate_attachment_metadata( $new_attachment_id, $mirror['file'] );

		wp_update_attachment_metadata( $new_attachment_id, $new_attachment_metadata );

		update_post_meta( $new_attachment_id, '_bricks_image_origin_url', $image_full_url );

		$new_image = [
			'id'       => $new_attachment_id,
			'filename' => $filename,
			'size'     => $image_size,
			'full'     => wp_get_attachment_image_url( $new_attachment_id, 'full' ),
			'url'      => wp_get_attachment_image_url( $new_attachment_id, $image_size ),
		];

		// Add to instance property to replace templateData before returning it to Vue
		self::$template_images[] = [
			'old' => $image,
			'new' => $new_image,
		];

		return $new_image;
	}

	/**
	 * Builder: Convert template data to new container layout structure
	 *
	 * @since 1.2
	 *
	 * @return void
	 */
	public function convert_template() {
		Ajax::verify_nonce( 'bricks-nonce-builder' );

		if ( ! Capabilities::current_user_has_full_access() ) {
			wp_send_json_error( 'verify_request: Sorry, you are not allowed to perform this action.' );
		}

		$elements      = ! empty( $_POST['templateData'] ) ? Ajax::decode( $_POST['templateData'], false ) : [];
		$import_images = isset( $_POST['importImages'] ) && $_POST['importImages'] == 'true';

		// Reset template instance data for new template insert
		self::$template_images = [];

		/**
		 * STEP: Convert container-based layout structure (@pre 1.5) to 'section', 'container', 'block' structure (@since 1.5)
		 *
		 * NOTE: Removed "Convert templates" Bricks setting @since 1.9.4
		 *
		 * On template import & insert.
		 */
		if ( isset( Database::$global_settings['convertTemplates'] ) ) {
			$converter_response = Converter::convert_container_to_section_block_element( $elements );

			if ( ! empty( $converter_response['elements'] ) ) {
				$elements = $converter_response['elements'];
			}
		}

		foreach ( $elements as $index => $element ) {
			if ( empty( $element['settings'] ) ) {
				$elements[ $index ]['settings'] = [];
			}

			// STEP: Import element images & update template data with local image data
			else {
				self::import_images( $element['settings'], $import_images );
			}

			if ( ! isset( $element['children'] ) ) {
				$elements[ $index ]['children'] = [];
			}
		}

		// STEP: Replace remote image data with imported/existing image data
		if ( count( self::$template_images ) ) {
			$elements_encoded = wp_json_encode( $elements );

			foreach ( self::$template_images as $template_image ) {
				$elements_encoded = str_replace(
					wp_json_encode( $template_image['old'] ),
					wp_json_encode( $template_image['new'] ),
					$elements_encoded
				);
			}

			$elements = json_decode( $elements_encoded, true );
		}

		// STEP: Generate new element IDs (@since 1.x)
		$elements = Helpers::generate_new_element_ids( $elements );

		wp_send_json_success( [ 'elements' => $elements ] );
	}

	/**
	 * Get the Templates list for the Template element (for the moment only Section and Content/Single template types)
	 */
	public static function get_templates_list( $template_types = '', $exclude_template_id = '' ) {
		$templates = self::get_templates_by_type( $template_types );

		$list = [];

		foreach ( $templates as $template_id ) {
			if ( $exclude_template_id == $template_id ) {
				continue;
			}

			$list[ $template_id ] = get_the_title( $template_id );
		}

		return $list;
	}

	/**
	 * Get IDs of all templates
	 *
	 * @see admin.php get_converter_items()
	 * @see files.php get_css_files_list()
	 *
	 * @param array $custom_args array Custom get_posts() arguments (@since 1.8; @see get_css_files_list).
	 *
	 * @since 1.4
	 */
	public static function get_all_template_ids( $custom_args = [] ) {
		$args = array_merge(
			[
				'post_type'              => BRICKS_DB_TEMPLATE_SLUG,
				'posts_per_page'         => -1,
				'post_status'            => 'any',
				'fields'                 => 'ids',
				'no_found_rows'          => true,
				'update_post_term_cache' => false,
				'meta_query'             => [
					'relation' => 'OR',
					[
						'key'     => BRICKS_DB_PAGE_HEADER,
						'value'   => '',
						'compare' => '!=',
					],
					[
						'key'     => BRICKS_DB_PAGE_CONTENT,
						'value'   => '',
						'compare' => '!=',
					],
					[
						'key'     => BRICKS_DB_PAGE_FOOTER,
						'value'   => '',
						'compare' => '!=',
					],
				],
			],
			$custom_args
		);

		return get_posts( $args );
	}

	/**
	 * Remove templates from /wp-sitemap.xml if not set to "Public templates" in Bricks settings
	 *
	 * @since 1.4
	 */
	public function remove_templates_from_wp_sitemap( $post_types ) {
		if ( ! isset( Database::$global_settings['publicTemplates'] ) ) {
			unset( $post_types[ BRICKS_DB_TEMPLATE_SLUG ] );
		}

		return $post_types;
	}

	/**
	 * Remove template taxonomies from /wp-sitemap.xml if not set to "Public templates" in Bricks settings
	 *
	 * @since 1.8
	 */
	public function remove_template_taxonomies_from_wp_sitemap( $taxonomies ) {
		if ( ! isset( Database::$global_settings['publicTemplates'] ) ) {
			unset( $taxonomies[ BRICKS_DB_TEMPLATE_TAX_TAG ] );
			unset( $taxonomies[ BRICKS_DB_TEMPLATE_TAX_BUNDLE ] );
		}

		return $taxonomies;
	}

	/**
	 * Frontend: Assign templates to hooks
	 *
	 * @since 1.9.1
	 */
	public function assign_templates_to_hooks() {
		// Return: In builder
		if ( bricks_is_builder() ) {
			return;
		}

		// STEP: Get all section templates
		$section_templates = self::get_templates_query(
			[
				'meta_query' => [
					[
						'key'     => BRICKS_DB_TEMPLATE_TYPE,
						'value'   => 'section',
						'compare' => '=',
					],
				],
			]
		);

		// Return: No section templates found
		if ( ! $section_templates->have_posts() ) {
			return;
		}

		// STEP: Loop over all section templates with 'Assign to hook' setting
		foreach ( $section_templates->posts as $section_template ) {
			$template_id       = $section_template->ID;
			$template_settings = Helpers::get_template_settings( $template_id );

			// Skip: Previewing the template itself
			if ( $template_id == get_the_ID() ) {
				continue;
			}

			$template_conditions = $template_settings['templateConditions'] ?? [];

			if ( empty( $template_conditions ) ) {
				continue;
			}

			// We need to group each condition by hookName and hookPriority
			$arranged_conditions = [];
			// STEP: rearrange conditions, group by hookName and hookPriority
			foreach ( $template_conditions as $condition ) {
				$hook_name     = $condition['hookName'] ?? false;
				$hook_priority = $condition['hookPriority'] ?? 10;

				// If hook name is not set, we skip this condition
				if ( ! $hook_name ) {
					continue;
				}

				$key = $hook_name . '|' . $hook_priority;

				if ( ! isset( $arranged_conditions[ $key ] ) ) {
					$arranged_conditions[ $key ] = [];
				}

				// Backward compatibility: If $condition['main'] === 'hook', set it as 'any' (run in entire website)
				$condition['main']             = $condition['main'] === 'hook' ? 'any' : $condition['main'];
				$arranged_conditions[ $key ][] = $condition;
			}

			// STEP: Decide if we need to add the template to the hook
			foreach ( $arranged_conditions as $key => $conditions ) {
				$hook_name     = explode( '|', $key )[0];
				$hook_priority = explode( '|', $key )[1];

				$run_hook = self::run_template_on_hook( $conditions );

				if ( ! $run_hook ) {
					continue;
				}

				// STEP: Add template to hook
				add_action(
					$hook_name,
					function() use ( $template_id ) {
						// Use [bricks_template] shortcode to render the template content (included styles)
						echo do_shortcode( "[bricks_template id='$template_id' on_hook='1']" );
					},
					$hook_priority
				);
			}
		}

	}

	/**
	 * Check if template should be run on hook
	 *
	 * @since 1.9.2
	 *
	 * @param array $arranged_conditions
	 *
	 * @return bool
	 */
	public static function run_template_on_hook( $arranged_conditions = [] ) {
		if ( empty( $arranged_conditions ) ) {
			return false;
		}

		$preview_type = '';
		$post_id      = Database::$page_data['post_id'];

		// Check if currently previewing a template
		if ( is_singular( BRICKS_DB_TEMPLATE_SLUG ) && isset( Database::$page_data['preview_or_post_id'] ) ) {
			$preview_type = Helpers::get_template_setting( 'templatePreviewType', Database::$page_data['preview_or_post_id'] );
			$post_id      = Database::$page_data['preview_or_post_id'];
		}

		$post_type = get_post_type( $post_id ); // Considered template preview as well

		$results = [
			'include' => [],
			'exclude' => [],
		];

		// STEP: Loop over all template conditions: If they are met, store results in $results
		foreach ( $arranged_conditions as $condition ) {
			if ( ! isset( $condition['main'] ) ) {
				continue;
			}

			// Reset condition met
			$condition_met = false;

			/**
			 * Possible values:
			 * any, frontpage, postType, archiveType, search, error, terms, ids
			 */
			$condition_type = $condition['main'];

			switch ( $condition_type ) {
				// Entire website
				case 'any':
					$condition_met = true;
					break;

				// Check for front page
				case 'frontpage':
					if ( bricks_is_ajax_call() || bricks_is_rest_call() ) {
						$front_page_id = get_option( 'page_on_front' );
						$is_front_page = absint( $post_id ) == absint( $front_page_id );
					} else {
						$is_front_page = is_front_page();
					}

					if ( $is_front_page ) {
						$condition_met = true;
					}
					break;

				// Check for a specific post type
				case 'postType':
					// Did not set any post types, skip
					if ( ! isset( $condition['postType'] ) ) {
						break;
					}

					// Check if the current post type matches any of the selected post types. $post_type considered template preview as well
					if ( in_array( $post_type, $condition['postType'] ) ) {
						$condition_met = true;
					}
					break;

				// Archive (any/author/data/term)
				case 'archiveType':
					if ( ! isset( $condition['archiveType'] ) ) {
						break;
					}

					// Archive pages include category, tag, author, date, custom post type, and custom taxonomy based archives.
					if ( in_array( 'any', $condition['archiveType'] ) && ( is_archive() || strpos( $preview_type, 'archive' ) !== false ) ) {
						$condition_met = true;
					}

					// Post type archive
					elseif ( in_array( 'postType', $condition['archiveType'] ) && ( is_post_type_archive() || $preview_type === 'archive-cpt' ) ) {
						if ( empty( $condition['archivePostTypes'] ) ) {
							// no post types set, any post type archive matches
							$condition_met = true;
						} else {

							// Previewing a template with content set to a CPT archive
							if ( $preview_type === 'archive-cpt' ) {
								$preview_cpt = Helpers::get_template_setting( 'templatePreviewPostType', $post_id );
								if ( $preview_cpt && in_array( $preview_cpt, $condition['archivePostTypes'] ) ) {
									$condition_met = true;
								}
							}
							// or, check if the post type archive matches the post type condition
							elseif ( is_post_type_archive( $condition['archivePostTypes'] ) ) {
								$condition_met = true;
							}

						}
					}

					// Author archive
					elseif ( in_array( 'author', $condition['archiveType'] ) && ( is_author() || $preview_type === 'archive-author' ) ) {
						$condition_met = true;
					}

					// Date archive
					elseif ( in_array( 'date', $condition['archiveType'] ) && ( is_date() || $preview_type === 'archive-date' ) ) {
						$condition_met = true;
					}

					// Term archive
					elseif ( in_array( 'term', $condition['archiveType'] ) && ( is_category() || is_tag() || is_tax() || $preview_type === 'archive-term' ) ) {
						if ( empty( $condition['archiveTerms'] ) ) {
							// no taxonomies set, any taxonomy archive matches
							$condition_met = true;
						} elseif ( is_array( $condition['archiveTerms'] ) ) {

							// Previewing a template, with populate content set to archive of term
							if ( $preview_type === 'archive-term' ) {
								// Note the post_id here is the template post Id (because in this archive situation the preview_id was not set)
								$preview_term = Helpers::get_template_setting( 'templatePreviewTerm', $post_id );

								if ( ! empty( $preview_term ) ) {
									$preview_term     = explode( '::', $preview_term );
									$queried_taxonomy = isset( $preview_term[0] ) ? $preview_term[0] : '';
									$queried_term_id  = isset( $preview_term[1] ) ? intval( $preview_term[1] ) : '';
								}
							}

							// All the other situations in frontend: is_category() || is_tag() || is_tax()
							else {
								$queried_object = get_queried_object();

								if ( is_object( $queried_object ) ) {
									$queried_term_id  = intval( $queried_object->term_id );
									$queried_taxonomy = $queried_object->taxonomy;
								}
							}

							// Check if queried taxonomy and term_id matches any of the selected archive terms
							if ( ! empty( $queried_term_id ) && ! empty( $queried_taxonomy ) ) {
								foreach ( $condition['archiveTerms'] as $archive_term ) {
									$term_parts = explode( '::', $archive_term );
									$taxonomy   = $term_parts[0];
									$term_id    = $term_parts[1];

									if ( $queried_taxonomy === $taxonomy ) {
										if ( $queried_term_id === intval( $term_id ) ) {
											$condition_met = true;
											break;
										}

										// Applied for taxonomy::all (all terms of a taxonomy)
										elseif ( 'all' == $term_id ) {
											$condition_met = true;
											break;
										}

										// The condition includes child terms, check if the queried term id is child of the term id set in the condition
										elseif ( isset( $condition['archiveTermsIncludeChildren'] ) && term_is_ancestor_of( $term_id, $queried_term_id, $queried_taxonomy ) ) {
											$condition_met = true;
											break;
										}
									}
								}
							}
						}
					}
					break;

				// Check for search
				case 'search':
					if ( is_search() || $preview_type === 'search' ) {
						$condition_met = true;
					}
					break;

				// Check for error
				case 'error':
					if ( is_404() || $preview_type === 'error' ) {
						$condition_met = true;
					}
					break;

				// Check for a specific term assigned to the post
				case 'terms':
					// Did not set any terms, skip
					if ( ! isset( $condition['terms'] ) || empty( $post_id ) ) {
						break;
					}

					$terms = $condition['terms'];

					foreach ( $terms as $term ) {
						$tax_term = explode( '::', $term );
						$taxonomy = $tax_term[0];
						$term     = $tax_term[1];

						$post_terms = wp_get_post_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] );

						if ( is_array( $post_terms ) && in_array( $term, $post_terms ) ) {
							$condition_met = true;
							break;
						}
					}
					break;

				// Check for a specific post ID or children
				case 'ids':
					// Did not set any post IDs, skip
					if ( ! isset( $condition['ids'] ) || empty( $post_id ) ) {
						break;
					}

					// Specif post ID
					if ( in_array( $post_id, $condition['ids'] ) ) {
						$condition_met = true;
						break;
					}

					// Apply to child pages
					elseif ( isset( $condition['idsIncludeChildren'] ) ) {
						$ancestors = get_post_ancestors( $post_id );

						foreach ( $ancestors as $ancestor_id ) {
							if ( in_array( $ancestor_id, $condition['ids'] ) ) {
								$condition_met = true;
								break;
							}
						}
					}
					break;
			}

			// Store condition result
			$exclude                          = isset( $condition['exclude'] );
			$include_or_exclude               = $exclude ? 'exclude' : 'include';
			$results[ $include_or_exclude ][] = [
				'condition_type' => $condition_type,
				'condition_met'  => $condition_met,
				'exclude'        => $exclude,
			];
		} // end foreach

		// STEP: Analyze results
		// If exclude is empty: user wants to insert the section to certain criteria. We return true if any of the conditions is true
		if ( empty( $results['exclude'] ) ) {
			$run_template = false;

			foreach ( $results['include'] as $result ) {
				if ( $result['condition_met'] ) {
					$run_template = true;
					break;
				}
			}
		}

		// If include is empty: user wants to insert the section to all pages, except certain criteria. We return true if all of the conditions are true
		elseif ( empty( $results['include'] ) ) {
			$run_template = true;

			foreach ( $results['exclude'] as $result ) {
				if ( $result['condition_met'] ) {
					$run_template = false;
					break;
				}
			}
		}

		// If both include and exclude are set, we return true if any of the include conditions is true and none of the exclude conditions is true
		else {
			$run_template = false;

			foreach ( $results['include'] as $result ) {
				if ( $result['condition_met'] ) {
					$run_template = true;
					break;
				}
			}

			foreach ( $results['exclude'] as $result ) {
				if ( $result['condition_met'] ) {
					$run_template = false;
					break;
				}
			}
		}

		return $run_template;
	}
}