<?php

/**
 * Builder data handling class that deals
 * with all database operations.
 *
 * @since 1.0
 */
final class FLBuilderModel {

	/**
	 * An array that contains the sizes for columns
	 * in each row layout.
	 *
	 * @since 1.0
	 * @var array $row_layouts
	 */
	static public $row_layouts = array(
		'1-col'              => array( 100 ),
		'2-cols'             => array( 50, 50 ),
		'3-cols'             => array( 33.33, 33.33, 33.33 ),
		'4-cols'             => array( 25, 25, 25, 25 ),
		'5-cols'             => array( 20, 20, 20, 20, 20 ),
		'6-cols'             => array( 16.65, 16.65, 16.65, 16.65, 16.65, 16.65 ),
		'left-sidebar'       => array( 33.33, 66.66 ),
		'right-sidebar'      => array( 66.66, 33.33 ),
		'left-right-sidebar' => array( 25, 50, 25 ),
	);

	/**
	 * An array that contains data for each registered settings form.
	 *
	 * @since 1.0
	 * @var array $settings_forms
	 */
	static public $settings_forms = array();

	/**
	 * An array used to cache default values for settings forms.
	 *
	 * @since 1.0
	 * @var array $settings_form_defaults
	 */
	static public $settings_form_defaults = array();

	/**
	 * An array of instances for each registered module.
	 *
	 * @since 1.0
	 * @var array $modules
	 */
	static public $modules = array();

	/**
	 * An array of module aliases with their own names,
	 * categories and default settings.
	 *
	 * @since 1.10
	 * @var array $module_aliases
	 */
	static public $module_aliases = array();

	/**
	 * Whether the builder is active or not.
	 *
	 * @since 1.10
	 * @access private
	 * @var bool $active
	 */
	static private $active = null;

	/**
	 * Cached global settings.
	 *
	 * @access private
	 * @var array $global_settings
	 */
	static private $global_settings;

	/**
	 * The last node id that was generated by the builder.
	 * This is saved to ensure the next node id is unique.
	 *
	 * @since 1.0
	 * @access private
	 * @var string $last_generated_node_id
	 */
	static private $last_generated_node_id = null;

	/**
	 * Cached post data from either the $_POST array
	 * or from the fl_builder_data post variable.
	 *
	 * @since 1.0
	 * @access private
	 * @var array $post_data
	 */
	static private $post_data = null;

	/**
	 * An array of post IDs the builder will be forced to use instead
	 * of a post ID set in the internal $post_data array or the global
	 * $post->ID when calling the FLBuilderModel::get_post_id method.
	 *
	 * The first post ID in the array is always returned for the
	 * FLBuilderModel::get_post_id method. To get a previously set
	 * post ID, first call the FLBuilderModel::reset_post_id method.
	 *
	 * @since 1.10
	 * @access private
	 * @var array $post_id
	 */
	static private $post_id = array();

	/**
	 * An array of cached published layout data by post_id.
	 *
	 * @since 1.0
	 * @access private
	 * @var array $published_layout_data
	 */
	static private $published_layout_data = array();

	/**
	 * An array of cached draft layout data by post_id.
	 *
	 * @since 1.0
	 * @access private
	 * @var array $draft_layout_data
	 */
	static private $draft_layout_data = array();

	/**
	 * An array of paths to template data files.
	 *
	 * @since 1.8
	 * @access private
	 * @var array $templates
	 */
	static private $templates = array();

	/**
	 * An array of cached template data that has been
	 * loaded from .dat files.
	 *
	 * @since 1.10
	 * @access private
	 * @var array $template_data
	 */
	static private $template_data = null;

	/**
	 * An array of cached post IDs for node templates.
	 *
	 * @since 1.7.6
	 * @access private
	 * @var array $node_template_post_ids
	 */
	static private $node_template_post_ids = array();

	/**
	 * An array of cached types for user and node templates.
	 *
	 * @since 1.7.9
	 * @access private
	 * @var array $node_template_types
	 */
	static private $node_template_types = array();

	static private $get_user_templates_cache = array();

	static private $categorized_child_nodes_cache = array();

	/**
	 * Initialize hooks.
	 *
	 * @since 1.8
	 * @return void
	 */
	static public function init() {
		/* Admin AJAX */
		add_action( 'wp_ajax_fl_builder_disable', __CLASS__ . '::disable' );
		add_action( 'wp_ajax_fl_builder_duplicate_wpml_layout', __CLASS__ . '::duplicate_wpml_layout' );

		/* Actions */
		add_action( 'init', __CLASS__ . '::load_settings', 1 );
		add_action( 'init', __CLASS__ . '::load_modules', 2 );
		add_action( 'wp', __CLASS__ . '::filter_settings_forms', 1 );
		add_action( 'before_delete_post', __CLASS__ . '::delete_post' );
		add_action( 'save_post', __CLASS__ . '::save_revision', 10, 3 );
		add_action( 'save_post', __CLASS__ . '::set_node_template_default_type', 10, 3 );
		add_action( 'wp_restore_post_revision', __CLASS__ . '::restore_revision', 10, 2 );
		add_action( 'fl_builder_after_save_layout', __CLASS__ . '::save_layout_revision' );
		add_action( 'fl_builder_after_save_user_template', __CLASS__ . '::save_layout_revision' );

		/* Filters */
		add_filter( 'fl_builder_get_global_settings', __CLASS__ . '::filter_global_settings', 10, 1 );
		add_filter( 'heartbeat_received', __CLASS__ . '::lock_post', 10, 2 );
		add_filter( 'fl_builder_register_settings_form', __CLASS__ . '::filter_row_settings_for_resize', 10, 2 );
		add_filter( 'wp_revisions_to_keep', __CLASS__ . '::limit_revisions', 10, 2 );
		add_action( 'fl_builder_after_render_row', array( __CLASS__, 'animation_opacity' ), 10, 2 );
		add_action( 'fl_builder_after_render_module', array( __CLASS__, 'animation_opacity' ), 10, 2 );
		add_action( 'fl_builder_after_render_col', array( __CLASS__, 'animation_opacity' ), 10, 2 );

		/* Core Templates */
		self::register_core_templates();
	}

	public static function animation_opacity( $node, $extra = false ) {
		if ( ! self::is_builder_active() && isset( $node->settings->animation ) && is_array( $node->settings->animation ) && ! empty( $node->settings->animation['style'] ) ) {
			printf( '<style>.fl-node-%s.fl-animation:not(.fl-animated){opacity:0}</style>', $node->node );
		}
	}

	/**
	 * Returns a builder edit URL for a post.
	 *
	 * @since 1.0
	 * @param int $post_id The post id to get an edit url for.
	 * @param bool $include_ui Return the full UI URL or just the layout.
	 * @return string
	 */
	static public function get_edit_url( $post_id = false, $include_ui = true ) {
		if ( false === $post_id ) {
			global $post;
		} else {
			$post = get_post( $post_id );
		}

		preg_match( '/(https?)/', get_bloginfo( 'url' ), $matches );

		$scheme = ( isset( $matches[1] ) ) ? $matches[1] : false;
		$url    = set_url_scheme( get_permalink( $post->ID ), $scheme );
		$url    = add_query_arg( 'fl_builder', '', $url );

		if ( FLBuilderUIIFrame::is_enabled() ) {
			if ( $include_ui ) {
				$url = add_query_arg( 'fl_builder_ui', '', $url );
			} else {
				/**
				 * Preserve any query args and pass to the iframe
				 */
				$args = array();
				parse_str( $_SERVER['QUERY_STRING'], $args );
				$args['fl_builder_ui_iframe'] = '';
				unset( $args['fl_builder_ui'] );
				$url = add_query_arg( $args, $url );
			}
		}

		/**
		 * Filter the bb edit url.
		 * @see fl_get_edit_url
		 * @param $url url
		 * @param $post post object
		 */
		return apply_filters( 'fl_get_edit_url', $url, $post );
	}

	/**
	 * Returns the URL to upgrade the builder to the premium version.
	 * Can be overridden by theme developers to use their affiliate
	 * link using the fl_builder_upgrade_url filter.
	 *
	 * @since 1.0
	 * @param array $params An array of key/value params to add to the query string.
	 * @return string
	 */
	static public function get_upgrade_url( $params = array() ) {
		/**
		 * Use this filter to modify the upgrade URL in Beaver Builder Lite.
		 * This can be used to add an affiliate ID.
		 * @see fl_builder_upgrade_url
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		return apply_filters( 'fl_builder_upgrade_url', self::get_store_url( '', $params ) );
	}

	/**
	 * Returns a URL that points to the Beaver Builder store.
	 *
	 * @since 1.8.6
	 * @param string $path A URL path to append to the store URL.
	 * @param array $params An array of key/value params to add to the query string.
	 * @return string
	 */
	static public function get_store_url( $path = '', $params = array() ) {
		$url = add_query_arg( $params, FL_BUILDER_STORE_URL . $path );
		return apply_filters( 'fl_builder_store_url', $url, $path );
	}

	/**
	 * Returns the relative URL for the plugin folder.
	 *
	 * @since 2.3
	 * @return string
	 */
	static public function get_relative_plugin_url() {
		$url         = str_ireplace( home_url(), '', FLBuilder::plugin_url() );
		$parsed_path = parse_url( FLBuilder::plugin_url(), PHP_URL_PATH );

		if ( strstr( $url, '://' ) && $parsed_path ) {
			$url = $parsed_path;
		}

		return $url;
	}

	/**
	 * Returns an array of post data from either $_POST['fl_builder_data']
	 * or $_POST if that is not set.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_post_data() {
		if ( ! self::$post_data ) {

			self::$post_data = array();

			if ( isset( $_POST['fl_builder_data'] ) ) {

				// Decode settings if our ModSecurity fix is enabled.
				if ( isset( $_POST['fl_builder_data']['settings'] ) ) {
					$_POST['fl_builder_data']['settings'] = FLBuilderUtils::modsec_fix_decode( $_POST['fl_builder_data']['settings'] );
				}
				if ( isset( $_POST['fl_builder_data']['node_settings'] ) ) {
					$_POST['fl_builder_data']['node_settings'] = FLBuilderUtils::modsec_fix_decode( $_POST['fl_builder_data']['node_settings'] );
				}

				if ( isset( $_POST['fl_builder_data']['node_preview'] ) ) {
					$_POST['fl_builder_data']['node_preview'] = FLBuilderUtils::modsec_fix_decode( $_POST['fl_builder_data']['node_preview'] );
				}

				$data = FLBuilderUtils::json_decode_deep( wp_unslash( $_POST['fl_builder_data'] ) );

				foreach ( $data as $key => $val ) {
					self::$post_data[ $key ] = $val;
				}
			} elseif ( isset( $_POST ) ) {

				foreach ( $_POST as $key => $val ) {
					self::$post_data[ $key ] = $val;
				}
			}
		}

		return self::$post_data;
	}

	/**
	 * Update a value in the $post_data array.
	 *
	 * @since 1.0
	 * @param string $key The post data key.
	 * @param mixed $value The value to update.
	 * @return void
	 */
	static public function update_post_data( $key, $value ) {
		$post_data         = self::get_post_data();
		$post_data[ $key ] = $value;
		self::$post_data   = $post_data;
	}

	/**
	 * Return an array of post types that the builder
	 * is enabled to work with.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_post_types() {
		$value = self::get_admin_settings_option( '_fl_builder_post_types', true );

		if ( ! $value ) {
			$value = array( 'page', 'fl-builder-template' );
		} else {
			$value[] = 'fl-builder-template';
		}

		/**
		 * Use this filter to modify the post types that the builder works with.
		 * @see fl_builder_post_types
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		return apply_filters( 'fl_builder_post_types', $value );
	}

	/**
	 * Return an array of post ids that should have their
	 * builder assets loaded globally.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_global_posts() {
		/**
		 * Use this filter to specify a post or posts whose CSS and JavaScript assets should be loaded globally.
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 * @see fl_builder_global_posts
		 * @since 1.0
		 */
		return apply_filters( 'fl_builder_global_posts', array() );
	}

	/**
	 * Adds the given post ID to the beginning of the internal $post_id
	 * array so the builder will be forced to use that instead of
	 * a post ID set in the internal $post_data array or the global $post->ID.
	 *
	 * @since 1.10
	 * @param int $post_id
	 * @return void
	 */
	static public function set_post_id( $post_id ) {
		array_unshift( self::$post_id, $post_id );
	}

	/**
	 * Removes the first item from the internal $post_id array so the
	 * last set post ID is used. If the internal $post_id array is
	 * empty, a post ID set in the internal $post_data array or the global
	 * $post->ID will be used when calling FLBuilderModel::get_post_id.
	 *
	 * @since 1.10
	 * @return void
	 */
	static public function reset_post_id() {
		array_shift( self::$post_id );
	}

	/**
	 * Returns the post id for the current post that
	 * is being displayed or worked on.
	 *
	 * @since 1.0
	 * @param bool $force_globals Force the use of WP globals instead of checking our internal post ID.
	 * @return int|bool The post id or false.
	 */
	static public function get_post_id( $force_globals = false ) {

		// Check our internal post IDs first if we're not forced to use WP globals.
		if ( ! $force_globals ) {

			$post_data = self::get_post_data();

			if ( ! empty( self::$post_id ) ) {
				// Get a post ID from the internal $post_id array if not empty.
				return (int) self::$post_id[0];
			} elseif ( isset( $post_data['post_id'] ) ) {
				// Get a post ID from an AJAX request.
				return (int) $post_data['post_id'];
			}
		}

		// Check WP globals.
		global $wp_the_query;
		global $post;

		if ( in_the_loop() && is_main_query() && isset( $wp_the_query->post ) && $wp_the_query->post instanceof WP_Post ) {
			// Get a post ID from the main query.
			return (int) $wp_the_query->post->ID;
		} elseif ( $post instanceof WP_Post ) {
			// Get a post ID in a query outside of the main loop.
			return (int) $post->ID;
		}

		// No post ID found.
		return false;
	}

	/**
	 * Returns the post object for the current post that
	 * is being worked on.
	 *
	 * @since 1.6.3
	 * @return object
	 */
	static public function get_post() {
		return get_post( self::get_post_id() );
	}

	/**
	 * Checks to see if the site has SSL enabled or not.
	 *
	 * @since 1.0
	 * @return bool
	 */
	static public function is_ssl() {
		if ( is_ssl() ) {
			return true;
		} elseif ( 0 === stripos( get_option( 'siteurl' ), 'https://' ) ) {
			return true;
		} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks to see if the builder can be enabled for
	 * the current post in the main query.
	 *
	 * @since 1.0
	 * @return bool
	 */
	static public function is_post_editable() {
		global $wp_the_query;

		$editable = false;

		if ( is_singular() && isset( $wp_the_query->post ) ) {

			$post        = $wp_the_query->post;
			$post_types  = self::get_post_types();
			$user_can    = current_user_can( 'edit_post', $post->ID );
			$user_access = FLBuilderUserAccess::current_user_can( 'builder_access' );

			if ( in_array( $post->post_type, $post_types ) && $user_can && $user_access ) {
				$editable = true;
			}
		}
		/**
		 * Checks to see if the builder can be enabled for
		 * the current post in the main query.
		 * @see fl_builder_is_post_editable
		 */
		return (bool) apply_filters( 'fl_builder_is_post_editable', $editable );
	}

	/**
	 * Filter global settings.
	 *
	 * @return object
	 */
	static public function filter_global_settings( $new_settings ) {
		// row margin.
		if ( isset( $new_settings->row_margins ) ) {
			$new_settings->row_margins_top    = $new_settings->row_margins;
			$new_settings->row_margins_right  = $new_settings->row_margins;
			$new_settings->row_margins_bottom = $new_settings->row_margins;
			$new_settings->row_margins_left   = $new_settings->row_margins;

			unset( $new_settings->row_margins );
		}

		if ( isset( $new_settings->row_margins_medium ) ) {
			$new_settings->row_margins_top_medium    = $new_settings->row_margins_medium;
			$new_settings->row_margins_right_medium  = $new_settings->row_margins_medium;
			$new_settings->row_margins_bottom_medium = $new_settings->row_margins_medium;
			$new_settings->row_margins_left_medium   = $new_settings->row_margins_medium;

			unset( $new_settings->row_margins_medium );
		}

		if ( isset( $new_settings->row_margins_responsive ) ) {
			$new_settings->row_margins_top_responsive    = $new_settings->row_margins_responsive;
			$new_settings->row_margins_right_responsive  = $new_settings->row_margins_responsive;
			$new_settings->row_margins_bottom_responsive = $new_settings->row_margins_responsive;
			$new_settings->row_margins_left_responsive   = $new_settings->row_margins_responsive;

			unset( $new_settings->row_margins_responsive );
		}

		// row padding.
		if ( isset( $new_settings->row_padding ) ) {
			$new_settings->row_padding_top    = $new_settings->row_padding;
			$new_settings->row_padding_right  = $new_settings->row_padding;
			$new_settings->row_padding_bottom = $new_settings->row_padding;
			$new_settings->row_padding_left   = $new_settings->row_padding;

			unset( $new_settings->row_padding );
		}

		if ( isset( $new_settings->row_padding_medium ) ) {
			$new_settings->row_padding_top_medium    = $new_settings->row_padding_medium;
			$new_settings->row_padding_right_medium  = $new_settings->row_padding_medium;
			$new_settings->row_padding_bottom_medium = $new_settings->row_padding_medium;
			$new_settings->row_padding_left_medium   = $new_settings->row_padding_medium;

			unset( $new_settings->row_padding_medium );
		}

		if ( isset( $new_settings->row_padding_responsive ) ) {
			$new_settings->row_padding_top_responsive    = $new_settings->row_padding_responsive;
			$new_settings->row_padding_right_responsive  = $new_settings->row_padding_responsive;
			$new_settings->row_padding_bottom_responsive = $new_settings->row_padding_responsive;
			$new_settings->row_padding_left_responsive   = $new_settings->row_padding_responsive;

			unset( $new_settings->row_padding_responsive );
		}

		// column margin.
		if ( isset( $new_settings->column_margins ) ) {
			$new_settings->column_margins_top    = $new_settings->column_margins;
			$new_settings->column_margins_right  = $new_settings->column_margins;
			$new_settings->column_margins_bottom = $new_settings->column_margins;
			$new_settings->column_margins_left   = $new_settings->column_margins;

			unset( $new_settings->column_margins );
		}

		if ( isset( $new_settings->column_margins_medium ) ) {
			$new_settings->column_margins_top_medium    = $new_settings->column_margins_medium;
			$new_settings->column_margins_right_medium  = $new_settings->column_margins_medium;
			$new_settings->column_margins_bottom_medium = $new_settings->column_margins_medium;
			$new_settings->column_margins_left_medium   = $new_settings->column_margins_medium;

			unset( $new_settings->column_margins_medium );
		}

		if ( isset( $new_settings->column_margins_responsive ) ) {
			$new_settings->column_margins_top_responsive    = $new_settings->column_margins_responsive;
			$new_settings->column_margins_right_responsive  = $new_settings->column_margins_responsive;
			$new_settings->column_margins_bottom_responsive = $new_settings->column_margins_responsive;
			$new_settings->column_margins_left_responsive   = $new_settings->column_margins_responsive;

			unset( $new_settings->column_margins_responsive );
		}

		// column padding.
		if ( isset( $new_settings->column_padding ) ) {
			$new_settings->column_padding_top    = $new_settings->column_padding;
			$new_settings->column_padding_right  = $new_settings->column_padding;
			$new_settings->column_padding_bottom = $new_settings->column_padding;
			$new_settings->column_padding_left   = $new_settings->column_padding;

			unset( $new_settings->column_padding );
		}

		if ( isset( $new_settings->column_padding_medium ) ) {
			$new_settings->column_padding_top_medium    = $new_settings->column_padding_medium;
			$new_settings->column_padding_right_medium  = $new_settings->column_padding_medium;
			$new_settings->column_padding_bottom_medium = $new_settings->column_padding_medium;
			$new_settings->column_padding_left_medium   = $new_settings->column_padding_medium;

			unset( $new_settings->column_padding_medium );
		}

		if ( isset( $new_settings->column_padding_responsive ) ) {
			$new_settings->column_padding_top_responsive    = $new_settings->column_padding_responsive;
			$new_settings->column_padding_right_responsive  = $new_settings->column_padding_responsive;
			$new_settings->column_padding_bottom_responsive = $new_settings->column_padding_responsive;
			$new_settings->column_padding_left_responsive   = $new_settings->column_padding_responsive;

			unset( $new_settings->column_padding_responsive );
		}

		// module margin.
		if ( isset( $new_settings->module_margins ) ) {
			$new_settings->module_margins_top    = $new_settings->module_margins;
			$new_settings->module_margins_right  = $new_settings->module_margins;
			$new_settings->module_margins_bottom = $new_settings->module_margins;
			$new_settings->module_margins_left   = $new_settings->module_margins;

			unset( $new_settings->module_margins );
		}

		if ( isset( $new_settings->module_margins_medium ) ) {
			$new_settings->module_margins_top_medium    = $new_settings->module_margins_medium;
			$new_settings->module_margins_right_medium  = $new_settings->module_margins_medium;
			$new_settings->module_margins_bottom_medium = $new_settings->module_margins_medium;
			$new_settings->module_margins_left_medium   = $new_settings->module_margins_medium;

			unset( $new_settings->module_margins_medium );
		}

		if ( isset( $new_settings->module_margins_responsive ) ) {
			$new_settings->module_margins_top_responsive    = $new_settings->module_margins_responsive;
			$new_settings->module_margins_right_responsive  = $new_settings->module_margins_responsive;
			$new_settings->module_margins_bottom_responsive = $new_settings->module_margins_responsive;
			$new_settings->module_margins_left_responsive   = $new_settings->module_margins_responsive;

			unset( $new_settings->module_margins_responsive );
		}

		return $new_settings;
	}

	/**
	 * Called by the heartbeat API. Lock the current post
	 * so only the current user can edit it.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function lock_post( $response, $data ) {
		if ( isset( $data['fl_builder_post_lock'] ) ) {

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

			wp_set_post_lock( $data['fl_builder_post_lock']['post_id'] );
		}
		return $response;
	}

	/**
	 * Checks to see if the builder layout is enabled
	 * for the current post.
	 *
	 * @since 1.0
	 * @param int $post_id A post ID to check otherwise, self::get_post_id will be used.
	 * @return bool
	 */
	static public function is_builder_enabled( $post_id = null ) {
		global $wp_the_query;

		// If in iframe preview return true as the post might not be a draft yet.
		if ( self::is_builder_draft_preview() ) {
			return true;
		}

		$query_id = ( isset( $wp_the_query->post->ID ) ) ? $wp_the_query->post->ID : false;
		$post_id  = $post_id ? $post_id : self::get_post_id();

		if ( ! is_admin() && post_password_required( $post_id ) ) {
			return false;
		} elseif ( self::is_builder_active() && $query_id === $post_id ) {
			return true;
		} else {
			$post_types = self::get_post_types();
			$post       = get_post( $post_id );

			if ( $post && in_array( $post->post_type, $post_types ) ) {
				return get_post_meta( $post->ID, '_fl_builder_enabled', true );
			}
		}

		return false;
	}

	/**
	 * Checks to see if the builder UI is active for
	 * the current post in the main query.
	 *
	 * @since 1.0
	 * @return bool
	 */
	static public function is_builder_active() {
		global $wp_the_query;
		global $post;

		$query_id = ( isset( $wp_the_query->post->ID ) ) ? $wp_the_query->post->ID : false;
		$post_id  = ( isset( $post->ID ) ) ? $post->ID : false;

		if ( null !== self::$active ) {
			return apply_filters( 'fl_builder_model_is_builder_active', self::$active );
		} elseif ( ! is_admin() && is_singular() && $query_id != $post_id ) {
			self::$active = false;
		} elseif ( is_customize_preview() ) {
			self::$active = false;
		} elseif ( self::is_post_editable() && ! is_admin() && ! post_password_required() ) {
			$post_data    = self::get_post_data();
			self::$active = isset( $_GET['fl_builder'] ) || isset( $post_data['fl_builder'] );
		}
		return apply_filters( 'fl_builder_model_is_builder_active', self::$active );
	}

	/**
	 * Resets the cached builder active state.
	 *
	 * @return void
	 */
	static public function reset_active() {
		self::$active = false;
	}

	/**
	 * Returns if this is a draft layout preview or not.
	 *
	 * @since 2.1
	 * @return bool
	 */
	static public function is_builder_draft_preview() {
		return is_user_logged_in() && isset( $_GET['fl_builder_preview'] );
	}

	/**
	 * Checks to see if this is the first time
	 * a user has launched the builder.
	 *
	 * @since 1.4.9
	 * @return bool
	 */
	static public function is_new_user() {
		if ( self::is_builder_active() ) {

			if ( FLBuilderUIIFrame::is_enabled() && ! FLBuilderUIIFrame::is_iframe_request() ) {
				return false;
			}
			$current_user = wp_get_current_user();
			$launched     = get_user_meta( $current_user->ID, '_fl_builder_launched', true );

			if ( empty( $launched ) ) {
				update_user_meta( $current_user->ID, '_fl_builder_launched', 1 );
				return true;
			}
		}

		return false;
	}

	/**
	 * Gets the status to use for working with nodes in
	 * the database. Returns draft if the builder is active,
	 * otherwise it returns published.
	 *
	 * @since 1.0
	 * @return string
	 */
	static public function get_node_status() {
		$status = self::is_builder_active() ? 'draft' : 'published';
		/**
		 * @see fl_builder_node_status
		 */
		return apply_filters( 'fl_builder_node_status', $status );
	}

	/**
	 * Enable the builder layout for the current post.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function enable() {
		update_post_meta( self::get_post_id(), '_fl_builder_enabled', true );
	}

	/**
	 * Disable the builder layout for the current post.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function disable() {
		if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'fl-enable-editor' ) ) {
			$post_id = self::get_post_id();
			$user_id = get_current_user_id();
			$post    = get_post( $post_id );
			if ( current_user_can( 'edit_others_posts' ) || ( $post->post_author === $user_id ) ) {
				update_post_meta( $post_id, '_fl_builder_enabled', false );
			}
		}
		exit;
	}

	/**
	 * Enable the builder editor for the main post in the query.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function enable_editing() {
		global $wp_the_query;

		if ( self::is_post_editable() && is_object( $wp_the_query->post ) ) {

			$post      = $wp_the_query->post;
			$published = self::get_layout_data( 'published' );
			$draft     = self::get_layout_data( 'draft' );
			/**
			 * Original post content from database
			 * @see fl_builder_migrated_post_content
			 */
			$content = apply_filters( 'fl_builder_migrated_post_content', $post->post_content );

			// Migrate existing post content to the builder?
			if ( empty( $published ) && empty( $draft ) && ! empty( $content ) ) {

				$row            = self::add_row();
				$cols           = self::get_nodes( 'column' );
				$col            = array_shift( $cols );
				$settings       = self::get_module_defaults( 'rich-text' );
				$settings->text = $content;

				self::add_module( 'rich-text', $settings, $col->node );
			} elseif ( empty( $draft ) ) {
				self::update_layout_data( $published, 'draft', $post->ID );
				self::update_layout_settings( self::get_layout_settings( 'published' ), 'draft', $post->ID );
			}

			// Delete old draft asset cache.
			self::delete_asset_cache();

			// Lock the post.
			require_once ABSPATH . 'wp-admin/includes/post.php';
			wp_set_post_lock( $post->ID );

			/**
			 * Allow devs to hook into when editing is enabled.
			 * @see fl_builder_editing_enabled
			 */
			do_action( 'fl_builder_editing_enabled' );
		}
	}

	/**
	 * Returns an array of paths for the upload directory
	 * of the current site.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_upload_dir() {
		$wp_info  = wp_upload_dir( null, false );
		$dir_name = basename( FL_BUILDER_DIR );

		// We use bb-plugin for the lite version as well.
		if ( 'beaver-builder-lite-version' == $dir_name ) {
			$dir_name = 'bb-plugin';
		}

		// SSL workaround.
		if ( self::is_ssl() ) {
			$wp_info['baseurl'] = str_ireplace( 'http://', 'https://', $wp_info['baseurl'] );
		}

		// Build the paths.
		$dir_info = array(
			'path' => $wp_info['basedir'] . '/' . $dir_name . '/',
			'url'  => $wp_info['baseurl'] . '/' . $dir_name . '/',
		);

		// Create the upload dir if it doesn't exist.
		if ( ! fl_builder_filesystem()->file_exists( $dir_info['path'] ) ) {

			// Create the directory.
			fl_builder_filesystem()->mkdir( $dir_info['path'] );

			// Add an index file for security.
			fl_builder_filesystem()->file_put_contents( $dir_info['path'] . 'index.html', '' );
		}

		/**
		 * Use this filter to modify the upload directory path and URL that the builder uses to store things like the cache and custom icons.
		 * @see fl_builder_get_upload_dir
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		return apply_filters( 'fl_builder_get_upload_dir', $dir_info );
	}

	/**
	 * Returns an array of paths for the cache directory
	 * of the current site.
	 *
	 * @since 1.0
	 * @param string $name The name of the cache directory to get paths for.
	 * @return array
	 */
	static public function get_cache_dir( $name = 'cache' ) {
		$upload_info = self::get_upload_dir();
		$allowed     = array( 'cache', 'icons' );

		// Make sure the dir name is allowed.
		if ( ! in_array( $name, $allowed ) ) {
			return false;
		}

		// Build the paths.
		$dir_info = array(
			'path' => $upload_info['path'] . $name . '/',
			'url'  => $upload_info['url'] . $name . '/',
		);

		// Create the cache dir if it doesn't exist.
		if ( ! fl_builder_filesystem()->file_exists( $dir_info['path'] ) ) {

			// Create the directory.
			fl_builder_filesystem()->mkdir( $dir_info['path'] );

			// Add an index file for security.
			fl_builder_filesystem()->file_put_contents( $dir_info['path'] . 'index.html', '' );
		}
		/**
		 * Use this filter to modify the cache directory path and URL that the builder uses to store cached images, JavaScript, and CSS files.
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 * @see fl_builder_get_cache_dir
		 */
		return apply_filters( 'fl_builder_get_cache_dir', $dir_info );
	}

	/**
	 * Returns the version number to be applied to the query string
	 * of a CSS or JS asset. If the builder is active a random hash
	 * is returned to prevent caching, otherwise a hash of the post
	 * update time is returned.
	 *
	 * @since 1.0
	 * @return string
	 */
	static public function get_asset_version( $path = false ) {
		$post_id = self::get_post_id();
		$active  = self::is_builder_active();
		$preview = self::is_builder_draft_preview();

		if ( $active || $preview ) {
			return md5( FLBuilderModel::uniqid() );
		} else {
			return $path ? md5( file_get_contents( $path ) ) : md5( get_post_modified_time( 'U', false, $post_id ) );
		}
	}


	/**
	 * Returns an array of paths for the CSS and JS assets
	 * of the current post.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_asset_info() {
		$post_data = self::get_post_data();
		$post_id   = self::get_post_id();
		$cache_dir = self::get_cache_dir();
		$active    = self::is_builder_active();
		$preview   = self::is_builder_draft_preview();

		/**
		 *
		 * @see fl_builder_get_asset_info_post_id
		 * @since 2.2.5
		 */
		$post_id = apply_filters( 'fl_builder_get_asset_info_post_id', $post_id, $post_data, $active, $preview );

		if ( isset( $post_data['node_preview'] ) ) {
			$suffix = '-layout-preview';
		} elseif ( $active || $preview ) {
			$suffix = '-layout-draft';
		} else {
			$suffix = '-layout';
		}

		$info = array(
			'css'             => $cache_dir['path'] . $post_id . $suffix . '.css',
			'css_url'         => $cache_dir['url'] . $post_id . $suffix . '.css',
			'css_partial'     => $cache_dir['path'] . $post_id . $suffix . '-partial.css',
			'css_partial_url' => $cache_dir['url'] . $post_id . $suffix . '-partial.css',
			'js'              => $cache_dir['path'] . $post_id . $suffix . '.js',
			'js_url'          => $cache_dir['url'] . $post_id . $suffix . '.js',
			'js_partial'      => $cache_dir['path'] . $post_id . $suffix . '-partial.js',
			'js_partial_url'  => $cache_dir['url'] . $post_id . $suffix . '-partial.js',
		);

		return $info;
	}

	/**
	 * Returns the method used to enqueue layout css and js assets.
	 * Possible values are 'file' and 'inline'. By default, the file
	 * method is used. Return true for the fl_builder_render_assets_inline
	 * filter to enable inline enqueuing.
	 *
	 * @since 2.1.5
	 * @return string
	 */
	static public function get_asset_enqueue_method() {
		/**
		 * Should assets be rendered inline
		 * @since 2.1.5
		 * @see fl_builder_render_assets_inline
		 */
		return apply_filters( 'fl_builder_render_assets_inline', false ) ? 'inline' : 'file';
	}

	/**
	 * Deletes either the preview, draft or live CSS and/or JS asset cache
	 * for the current post based on the data returned from get_asset_info.
	 * Both the CSS and JS asset cache will be delete if a type is not specified.
	 *
	 * @since 1.0
	 * @param string $type The type of cache to delete. Either css or js.
	 * @return void
	 */
	static public function delete_asset_cache( $type = false ) {

		if ( 'inline' === FLBuilderModel::get_asset_enqueue_method() ) {
			return false;
		}

		$info  = self::get_asset_info();
		$types = $type ? array( $type ) : array( 'css', 'css_partial', 'js', 'js_partial' );

		foreach ( $types as $type ) {

			if ( isset( $info[ $type ] ) && fl_builder_filesystem()->file_exists( $info[ $type ] ) ) {
				fl_builder_filesystem()->unlink( $info[ $type ] );
			}
		}
	}

	/**
	 * Deletes preview, draft and live CSS/JS asset cache for the current
	 * post. If a post ID is supplied, the asset cache will be deleted for
	 * that post instead.
	 *
	 * @since 1.0
	 * @param int $post_id
	 * @return void
	 */
	static public function delete_all_asset_cache( $post_id = false ) {
		$post_id   = $post_id ? $post_id : self::get_post_id();
		$cache_dir = self::get_cache_dir();

		if ( $post_id && 'file' === FLBuilderModel::get_asset_enqueue_method() ) {

			$paths = array(
				$cache_dir['path'] . $post_id . '-layout.css',
				$cache_dir['path'] . $post_id . '-layout-draft.css',
				$cache_dir['path'] . $post_id . '-layout-preview.css',
				$cache_dir['path'] . $post_id . '-layout-partial.css',
				$cache_dir['path'] . $post_id . '-layout-draft-partial.css',
				$cache_dir['path'] . $post_id . '-layout-preview-partial.css',
				$cache_dir['path'] . $post_id . '-layout.js',
				$cache_dir['path'] . $post_id . '-layout-draft.js',
				$cache_dir['path'] . $post_id . '-layout-preview.js',
				$cache_dir['path'] . $post_id . '-layout-partial.js',
				$cache_dir['path'] . $post_id . '-layout-draft-partial.js',
				$cache_dir['path'] . $post_id . '-layout-preview-partial.js',
			);

			foreach ( $paths as $path ) {
				if ( fl_builder_filesystem()->file_exists( $path ) ) {
					fl_builder_filesystem()->unlink( $path );
				}
			}
		}
	}

	/**
	 * Deletes the asset cache for all posts that contain the node
	 * template with the supplied post ID.
	 *
	 * @since 1.6.3
	 * @param int $post_id
	 * @return void
	 */
	static public function delete_node_template_asset_cache( $post_id = false ) {
		$posts = self::get_posts_with_global_node_template( $post_id );

		if ( ! empty( $posts ) ) {
			foreach ( $posts as $post ) {
				self::delete_all_asset_cache( $post->ID );
			}
		}
	}

	/**
	 * Deletes preview, draft and live CSS/JS asset cache for all posts.
	 *
	 * @since 1.6.3
	 * @return void
	 */
	static public function delete_asset_cache_for_all_posts( $parts = '*' ) {
		$cache_dir = self::get_cache_dir();
		$css       = glob( $cache_dir['path'] . $parts . '.css' );
		$js        = glob( $cache_dir['path'] . $parts . '.js' );
		if ( is_array( $css ) ) {
			array_map( array( fl_builder_filesystem(), 'unlink' ), $css );
		}
		if ( is_array( $js ) ) {
			array_map( array( fl_builder_filesystem(), 'unlink' ), $js );
		}
	}

	/**
	 * Generates a unique id for a builder node such as a
	 * row, column or module.
	 *
	 * @since 1.0
	 * @return string
	 */
	static public function generate_node_id() {
		$node_id = FLBuilderModel::uniqid();

		if ( $node_id == self::$last_generated_node_id ) {
			return self::generate_node_id();
		}

		self::$last_generated_node_id = $node_id;

		return $node_id;
	}

	/**
	 * Generates new node ids for an array of nodes.
	 *
	 * @since 1.0
	 * @param array $data An array of node data.
	 * @return array
	 */
	static public function generate_new_node_ids( $data ) {
		$map   = array();
		$nodes = array();

		// Map the new node ids to the old.
		foreach ( $data as $node_id => $node ) {
			$map[ $node_id ] = self::generate_node_id();
		}

		// Replace the old node ids.
		foreach ( $data as $node_id => $node ) {

			$nodes[ $map[ $node_id ] ]       = $node;
			$nodes[ $map[ $node_id ] ]->node = $map[ $node_id ];

			if ( ! empty( $node->parent ) && isset( $map[ $node->parent ] ) ) {
				$nodes[ $map[ $node_id ] ]->parent = $map[ $node->parent ];
			}
		}

		return $nodes;
	}

	/**
	 * Returns a single node.
	 *
	 * @since 1.0
	 * @param string|object $node_id Either a node id or node object.
	 * @param string $status The node status. Either draft or published.
	 * @return object
	 */
	static public function get_node( $node_id = null, $status = null ) {
		if ( is_object( $node_id ) ) {
			$node = $node_id;
		} elseif ( null !== $node_id ) {
			$data = self::get_layout_data( $status );
			$node = isset( $data[ $node_id ] ) ? $data[ $node_id ] : null;
		} else {
			$node = false;
		}

		if ( $node && ! empty( $node->settings ) ) {
			$node->settings = self::get_node_settings( $node );
		}

		return $node;
	}

	/**
	 * Returns an array of nodes.
	 *
	 * @since 1.0
	 * @param string $type The type of nodes to return.
	 * @param string|object $parent_id Either the parent node id or parent node object.
	 * @param string $status The node status. Either draft or published.
	 * @return array
	 */
	static public function get_nodes( $type = null, $parent_id = null, $status = null ) {
		$parent = is_object( $parent_id ) ? $parent_id : self::get_node( $parent_id );
		$nodes  = array();

		// Get the layout data.
		if ( ! $parent ) {
			$data = self::get_layout_data( $status );
		} else {
			$data = self::get_child_nodes( $parent, $status );
		}

		// Return all nodes?
		if ( ! $type ) {
			$nodes = $data;
		} else {

			// Return nodes of a certain type.
			foreach ( $data as $node_id => $node ) {
				if ( ! $parent && ! $node->parent && 'row' === $type && 'module' === $node->type ) {
					// Handle container modules in the main layout as rows.
					if ( isset( self::$modules[ $node->settings->type ] ) && is_object( self::$modules[ $node->settings->type ] ) && self::$modules[ $node->settings->type ]->accepts_children() ) {
						$nodes[ $node_id ] = $node;
					}
				} elseif ( $node->type === $type ) {
					// Handle child nodes of a certain type.
					$nodes[ $node_id ] = $node;
				}
			}
		}

		// Sort the nodes by position.
		uasort( $nodes, array( 'FLBuilderModel', 'order_nodes' ) );

		// Merge default settings.
		foreach ( $nodes as $node_id => $node ) {

			if ( ! empty( $node->settings ) ) {
				$nodes[ $node_id ]->settings = self::get_node_settings( $nodes[ $node_id ] );
			}
		}

		// Return the nodes.
		return $nodes;
	}

	/**
	 * Returns the direct parent object for a single node.
	 *
	 * @since 1.9
	 * @param string|object $node_id Either a node id or node object.
	 * @param string $status The node status. Either draft or published.
	 * @return object
	 */
	static public function get_node_parent( $node_id = null, $status = null ) {
		$parent = null;

		if ( is_object( $node_id ) ) {
			$node = $node_id;
		} else {
			$node = self::get_node( $node_id, $status );
		}

		if ( $node ) {

			$template_post_id = self::is_node_global( $node );
			$post_id          = $template_post_id ? $template_post_id : self::get_post_id();
			$data             = self::get_layout_data( $status, $post_id );

			if ( isset( $data[ $node->parent ] ) ) {
				return $data[ $node->parent ];
			}
		}

		return $parent;
	}

	/**
	 * Returns a node's parent node of the specified type.
	 *
	 * @since 1.8.3
	 * @param string|object $node The node ID. Can also be a node object.
	 * @param string $type The type of parent to return. Either "column", "column-group" or "row".
	 * @return object The parent node.
	 */
	static public function get_node_parent_by_type( $node, $type = '' ) {
		// Get node object if node ID set
		if ( ! is_object( $node ) ) {
			$node = self::get_node( $node );
		}

		// Return early if no node object found or node has no parent
		if ( empty( $node ) || empty( $node->parent ) ) {
			return;
		}

		// Helper array of parent types and their categories for each node type
		$parent_types = array(
			'module'       => array(
				'type'     => 'column',
				'category' => 'columns',
			),
			'column'       => array(
				'type'     => 'column-group',
				'category' => 'groups',
			),
			'column-group' => array(
				'type'     => 'row',
				'category' => 'rows',
			),
		);

		// Helper array of node type hierarchies
		$hierarchy = array(
			'module'       => 10,
			'column'       => 20,
			'column-group' => 30,
			'row'          => 40,
		);

		// Set immediate parent type of the node when:
		// - type is not of allowed types
		// - type is the same as node type
		// - type is lower in hierarchy than the node type
		if ( ! in_array( $type, array_keys( $hierarchy ) ) || $type == $node->type || $hierarchy[ $parent_types[ $node->type ]['type'] ] > $hierarchy[ $type ] ) {
			$type = $parent_types[ $node->type ]['type'];
		}

		// Get all layout nodes, categorized
		$nodes = array_filter( self::get_categorized_nodes() );

		// Null out the output initially
		$output = '';

		// Parse layout nodes to get the correct output
		if ( ! empty( $nodes ) ) {
			while ( empty( $output ) ) {
				if ( ! empty( $node->parent ) && isset( $nodes[ $parent_types[ $node->type ]['category'] ] ) ) {

					$break_while = true;

					foreach ( $nodes[ $parent_types[ $node->type ]['category'] ] as $parent ) {
						if ( $parent->node == $node->parent ) {

							$break_while = false;

							if ( $parent_types[ $node->type ]['type'] == $type ) {
								// We have got the type we wanted! Set the output and break from while and foreach loops.
								$output = $parent;
								break; // From foreach
							}

							// We now need node parents to crawl the tree
							$node = $parent;
							break; // From foreach

						}
					}

					// If we get this far without changing $break_while, something is wrong
					if ( $break_while ) {
						break; // From while
					}
				} else {
					break; // From while
				}
			}
		}

		return $output;
	}

	/**
	 * Returns an array of child nodes for a parent.
	 *
	 * @since 1.0
	 * @param string|object $parent_id Either the parent node id or parent node object.
	 * @param string $status The node status. Either draft or published.
	 * @return array
	 */
	static public function get_child_nodes( $parent_id, $status = null ) {
		$parent           = is_object( $parent_id ) ? $parent_id : self::get_node( $parent_id );
		$template_post_id = self::is_node_global( $parent );
		$template_node_id = null;
		$status           = $template_post_id && ! self::is_post_node_template() ? 'published' : $status;
		$data             = self::get_layout_data( $status, $template_post_id );
		$nodes            = array();

		if ( $template_post_id ) {
			$template_node_id = apply_filters( 'fl_builder_parent_template_node_id', $parent->template_node_id, $parent, $data );
		}

		if ( is_object( $parent ) ) {
			foreach ( $data as $node_id => $node ) {
				$is_child        = isset( $node->parent ) && $node->parent == $parent->node;
				$is_global_child = $template_node_id && $template_node_id == $node->parent;

				if ( $is_global_child ) {
					$global_node                 = FLBuilderDynamicGlobal::merge_child_settings( $parent, $node );
					$nodes[ $global_node->node ] = $global_node;
				} elseif ( $is_child ) {
					$nodes[ $node_id ] = $node;
				}
			}
		}

		return $nodes;
	}

	/**
	 * Returns all child nodes and children of those children
	 * for a single node.
	 *
	 * @since 1.6.3
	 * @param string $parent_id The parent node id.
	 * @return array
	 */
	static public function get_nested_nodes( $parent_id ) {
		$children = self::get_child_nodes( $parent_id );

		foreach ( $children as $child_id => $child ) {

			$grand_children = self::get_child_nodes( $child_id );

			if ( count( $grand_children ) > 0 ) {

				$children = array_merge( $children, $grand_children );

				foreach ( $grand_children as $grand_child_id => $grand_child ) {

					$nested = self::get_nested_nodes( $grand_child_id );

					if ( count( $nested ) > 0 ) {

						$children = array_merge( $children, $nested );
					}
				}
			}
		}

		return $children;
	}

	/**
	 * Returns an array of all nodes for a layout, categorized by type.
	 *
	 * @since 1.6.3
	 * @return array
	 */
	static public function get_categorized_nodes() {
		if ( self::is_post_user_template( 'column' ) ) {
			$root_col                            = self::get_node_template_root( 'column' );
			$nodes                               = self::get_categorized_child_nodes( $root_col );
			$nodes['columns'][ $root_col->node ] = $root_col;
		} else {
			$nodes = self::get_categorized_child_nodes();
		}

		return $nodes;
	}

	/**
	 * Returns an array of all nodes for a parent node, categorized by type.
	 *
	 * @param string $parent The parent node.
	 * @return array
	 */
	static public function get_categorized_child_nodes( $parent = null ) {
		$categorized = array(
			'rows'    => array(),
			'groups'  => array(),
			'columns' => array(),
			'modules' => array(),
		);

		$nodes = self::get_nodes( null, $parent );

		foreach ( $nodes as $node ) {
			switch ( $node->type ) {
				case 'row':
					$categorized['rows'][ $node->node ] = $node;
					break;
				case 'column-group':
					$categorized['groups'][ $node->node ] = $node;
					break;
				case 'column':
					$categorized['columns'][ $node->node ] = $node;
					break;
				case 'module':
					$module = self::get_module( $node );
					if ( $module ) {
						$categorized['modules'][ $node->node ] = $module;
					}
					break;
			}

			$accepts_children = false;

			if ( 'module' !== $node->type ) {
				$accepts_children = true;
			} else {
				$module           = self::get_module( $node );
				$accepts_children = $module && $module->accepts_children();
			}

			if ( $accepts_children && self::is_node_global( $node ) ) {

				// Cache key must be instance-specific ($node->node) not template-specific
				// ($node->template_node_id) so multiple instances of the same component
				// get their own unique suffixed child node IDs for CSS targeting.
				$cache_key = $node->node;
				if ( isset( self::$categorized_child_nodes_cache[ $cache_key ] ) ) {
					$children = self::$categorized_child_nodes_cache[ $cache_key ];
				} else {
					$children = self::get_categorized_child_nodes( $node );
					self::$categorized_child_nodes_cache[ $cache_key ] = $children;
				}

				$categorized['rows']    = array_merge( $categorized['rows'], $children['rows'] );
				$categorized['groups']  = array_merge( $categorized['groups'], $children['groups'] );
				$categorized['columns'] = array_merge( $categorized['columns'], $children['columns'] );
				$categorized['modules'] = array_merge( $categorized['modules'], $children['modules'] );
			}
		}

		return $categorized;
	}

	/**
	 * Returns node settings that are merged with the
	 * default or preview settings.
	 *
	 * @since 1.0
	 * @param object|string $node A node object or node ID.
	 * @param bool $filter Whether to filter the settings or not.
	 * @return object
	 */
	static public function get_node_settings( $node, $filter = true ) {
		$node              = is_object( $node ) ? $node : self::get_node( $node );
		$post_data         = self::get_post_data();
		$is_dynamic_global = isset( $node->settings->dynamic_node_settings );

		// Get the node settings for a node template's root node?
		if ( ! $is_dynamic_global && self::is_node_template_root( $node ) && ! self::is_post_node_template( false, $node->type ) ) {
			$template_post_id = self::get_node_template_post_id( $node->template_id );
			$template_data    = self::get_layout_data( 'published', $template_post_id );

			// Fallback to draft data if we don't have published data.
			if ( ! isset( $template_data[ $node->template_node_id ] ) ) {
				$template_data = self::get_layout_data( 'draft', $template_post_id );
			}

			// Set the node settings to the template node settings.
			if ( isset( $template_data[ $node->template_node_id ] ) ) {
				$template_node     = $template_data[ $node->template_node_id ];
				$template_settings = clone $template_node->settings;

				if ( 'column' == $node->type ) {
					$template_settings->size = $node->settings->size;
				}

				$node->settings = $template_settings;

				if ( isset( $node->settings->dynamic_fields ) ) {
					unset( $node->settings->dynamic_fields );
				}
			}
		}

		// Check if we have a node preview ID. This is used to preview
		// node settings before they are saved. The `node_preview` key will
		// contain the settings, while either `node_preview_id` or `node_id`
		// will point to the node that should use the preview settings.
		$node_preview_id = null;

		if ( isset( $post_data['node_preview'] ) ) {
			if ( isset( $post_data['node_preview_id'] ) ) {
				$node_preview_id = $post_data['node_preview_id'];
			} elseif ( isset( $post_data['node_id'] ) ) {
				$node_preview_id = $post_data['node_id'];
			}
		}

		// Get either the preview settings or saved node settings merged with the defaults.
		if ( $node_preview_id && $node_preview_id == $node->node ) {

			if ( ! isset( $post_data['node_preview_processed_settings'] ) ) {
				$settings = $post_data['node_preview'];

				if ( isset( $settings['dynamic_node_settings'] ) ) {
					$settings = FLBuilderDynamicGlobal::merge_settings_for_save( $node, (object) $settings );
				}

				$settings = (object) array_merge( (array) $node->settings, (array) $settings );
				$settings = self::process_node_settings( $node, $settings );
				self::update_post_data( 'node_preview_processed_settings', $settings );
			} else {
				$settings = $post_data['node_preview_processed_settings'];
			}
		} else {
			$settings = self::get_node_settings_with_defaults_merged( $node );
		}

		$settings = FLBuilderDynamicGlobal::merge_settings_with_template( $node, $settings );

		// Refresh cached row video data for component instances where
		// the video attachment ID may have been overridden by the instance.
		if ( 'row' === $node->type
			&& ! empty( $node->template_node_id )
			&& 'video' === ( $settings->bg_type ?? '' )
			&& 'wordpress' === ( $settings->bg_video_source ?? '' )
		) {
			$settings = self::process_row_settings( $node, $settings );
		}

		return ! $filter ? $settings : apply_filters( 'fl_builder_node_settings', $settings, $node );
	}

	/**
	 * Returns node settings that are merged with the defaults. In general,
	 * you should use get_node_settings instead of this method unless you
	 * don't want any of the other logic that it applies.
	 *
	 * @since 2.2
	 * @param object $nodes A node object.
	 * @return object
	 */
	static public function get_node_settings_with_defaults_merged( $node ) {
		$type     = $node->type;
		$settings = $node->settings;
		$defaults = array();

		if ( 'row' == $type ) {
			$defaults = FLBuilderModel::get_row_defaults();
		} elseif ( 'column' == $type ) {
			$defaults = FLBuilderModel::get_col_defaults();
		} elseif ( 'module' == $type ) {
			$defaults = FLBuilderModel::get_module_defaults( $node );
		}

		$settings = (object) array_merge( (array) $defaults, (array) $settings );

		if ( 'row' == $type ) {
			$settings = FLBuilderModel::merge_nested_form_defaults( 'general', 'row', $settings );
		} elseif ( 'column' == $type ) {
			$settings = FLBuilderModel::merge_nested_form_defaults( 'general', 'col', $settings );
		} elseif ( 'module' == $type ) {
			$settings = FLBuilderModel::merge_nested_module_defaults( $settings->type, $settings );
		}

		return $settings;
	}

	/**
	 * Returns node settings that have been processed with
	 * specific logic based on the type of node.
	 *
	 * @since 1.0
	 * @param object $node A node object.
	 * @param object $new_settings The new node settings.
	 * @return object
	 */
	static public function process_node_settings( $node, $new_settings ) {

		if ( 'row' == $node->type ) {
			$new_settings = self::process_row_settings( $node, $new_settings );
			$new_settings = self::sanitize_settings( $new_settings, 'row', 'general' );
		}
		if ( 'column' == $node->type ) {
			$new_settings = self::process_col_settings( $node, $new_settings );
			$new_settings = self::sanitize_settings( $new_settings, 'col', 'general' );
		}
		if ( 'module' == $node->type ) {
			$new_settings = self::process_module_settings( $node, $new_settings );
			$new_settings = self::sanitize_settings( $new_settings, $node->settings->type, 'module' );
		}

		return $new_settings;
	}

	/**
	 * Returns the default settings for a node.
	 *
	 * @since 1.0
	 * @param object $node A node object.
	 * @return object
	 */
	static public function get_node_defaults( $node ) {
		$defaults = array();

		if ( 'row' == $node->type ) {
			$defaults = self::get_row_defaults();
		} elseif ( 'column' == $node->type ) {
			$defaults = self::get_col_defaults();
		} elseif ( 'module' == $node->type ) {
			$defaults = self::get_module_defaults( $node->settings->type );
		}

		return $defaults;
	}

	/**
	 * Get the placeholder data for node spacing breakpoint fields.
	 *
	 * @since 2.6
	 * @param string $type The type of node.
	 * @param string $property Either padding or margins.
	 * @param string $size The breakpoint size key.
	 * @return object
	 */
	static public function get_node_spacing_breakpoint_placeholders( $type, $property, $size ) {
		$global_settings = self::get_global_settings();
		$sizes           = array( 'large', 'medium', 'responsive' );
		$fallbacks       = array_reverse( array_slice( $sizes, 0, array_search( $size, $sizes ) ) );
		$sides           = array( 'top', 'right', 'bottom', 'left' );
		$placeholders    = array();

		foreach ( $sides as $side ) {
			$key                   = $type . '_' . $property . '_' . $side . '_' . $size;
			$placeholders[ $side ] = '';

			if ( '' === $global_settings->{ $key } ) {
				foreach ( $fallbacks as $fallback ) {
					$fallback_key = $type . '_' . $property . '_' . $side . '_' . $fallback;

					if ( '' !== $global_settings->{ $fallback_key } ) {
						$placeholders[ $side ] = $global_settings->{ $fallback_key };
						break;
					}
				}
			} else {
				$placeholders[ $side ] = $global_settings->{ $key };
			}
		}

		return $placeholders;
	}

	/**
	 * Returns the options for the responsive_display setting
	 * for all node types.
	 *
	 * @since 2.9
	 * @return array
	 */
	static public function get_node_breakpoint_options() {
		$options = [
			'desktop' => '<i class="dashicons dashicons-desktop"></i>',
			'large'   => '<i class="dashicons dashicons-laptop"></i>',
			'medium'  => '<i class="dashicons dashicons-tablet"></i>',
			'mobile'  => '<i class="dashicons dashicons-smartphone"></i>',
		];

		if ( FLBuilderModuleBlocks::is_block_editor_loading_config() ) {
			unset( $options['large'] );
		}

		return $options;
	}

	/**
	 * Returns the tooltips for the responsive_display setting
	 * for all node types.
	 *
	 * @since 2.9
	 * @return array
	 */
	static public function get_node_breakpoint_tooltips() {
		$tooltips = [
			'desktop' => __( 'Extra Large', 'fl-builder' ),
			'large'   => __( 'Large', 'fl-builder' ),
			'medium'  => __( 'Medium', 'fl-builder' ),
			'mobile'  => __( 'Mobile', 'fl-builder' ),
		];

		if ( FLBuilderModuleBlocks::is_block_editor_loading_config() ) {
			$tooltips = [
				'desktop' => __( 'Desktop', 'fl-builder' ),
				'medium'  => __( 'Tablet', 'fl-builder' ),
				'mobile'  => __( 'Mobile', 'fl-builder' ),
			];
		}

		return $tooltips;
	}

	/**
	 * Callback for the uasort function.
	 *
	 * @since 1.0
	 * @param int $a The first position.
	 * @param int $b The second position.
	 * @return int
	 */
	static public function order_nodes( $a, $b ) {
		return (int) $a->position - (int) $b->position;
	}

	/**
	 * Counts the number of nodes in a parent.
	 *
	 * @since 1.0
	 * @param string $type The type of nodes to count.
	 * @param string $parent_id The parent node id.
	 * @return int
	 */
	static public function count_nodes( $type = 'row', $parent_id = null ) {
		return count( self::get_nodes( $type, $parent_id ) );
	}

	/**
	 * Returns the index of the next available
	 * position in a parent node.
	 *
	 * @since 1.0
	 * @param string $type The type of nodes to count.
	 * @param string $parent_id The parent node id.
	 * @return int
	 */
	static public function next_node_position( $type = 'row', $parent_id = null ) {
		$nodes = self::get_nodes( $type, $parent_id );
		$last  = array_pop( $nodes );

		return $last ? $last->position + 1 : 0;
	}

	/**
	 * Deletes a node.
	 *
	 * @since 1.0
	 * @param string $node_id The ID of the node to delete.
	 * @return void
	 */
	static public function delete_node( $node_id = null ) {
		// Get the layout data.
		$data = self::get_layout_data();

		// Return if the node doesn't exist.
		if ( ! isset( $data[ $node_id ] ) ) {
			return;
		}

		// Get the node.
		$node = $data[ $node_id ];

		// Call the delete method if we're deleting a module.
		self::call_module_delete( $node );

		// Delete the node.
		unset( $data[ $node_id ] );

		// Get the sibling nodes.
		if ( 'row' === $node->type ) {
			$siblings = self::get_nodes( 'row' );
		} else {
			$siblings = self::get_nodes( null, $node->parent );
		}

		// Reorder sibling nodes.
		$position = 0;

		foreach ( $siblings as $sibling_id => $sibling ) {
			if ( isset( $data[ $sibling_id ] ) ) {
				$data[ $sibling_id ]->position = $position;
				$position++;
			}
		}

		// Delete the node's children.
		self::delete_child_nodes_from_data( $node, $data );

		// Update the layout data.
		self::update_layout_data( $data );
	}

	/**
	 * Deletes all child nodes for a parent.
	 *
	 * @since 1.0
	 * @param object $parent The parent node object.
	 * @param object $data The data array to delete from.
	 * @return void
	 */
	static public function delete_child_nodes_from_data( $parent, &$data ) {
		$children = self::get_nodes( null, $parent );

		foreach ( $children as $child_id => $child ) {

			// Call the delete method if we're deleting a module.
			self::call_module_delete( $child );

			// Delete the node.
			unset( $data[ $child_id ] );

			// Delete the node's children.
			self::delete_child_nodes_from_data( $child, $data );
		}
	}

	/**
	 * Calls the delete method for a node
	 * that is a module.
	 *
	 * @since 1.0
	 * @param object $node A module node.
	 * @return void
	 */
	static public function call_module_delete( $node ) {
		if ( 'module' == $node->type && isset( self::$modules[ $node->settings->type ] ) ) {
			$class              = get_class( self::$modules[ $node->settings->type ] );
			$instance           = new $class();
			$instance->node     = $node->node;
			$instance->parent   = $node->parent;
			$instance->settings = $node->settings;
			$instance->delete();
			$instance->remove();
		}
	}

	/**
	 * Repositions a node within a parent.
	 *
	 * @since 1.0
	 * @param string $node_id A node ID.
	 * @param int $position The new position.
	 * @param string $type The type of node to order.
	 * @return void
	 */
	static public function reorder_node( $node_id = null, $position = 0 ) {
		$data    = self::get_layout_data();
		$node    = $data[ $node_id ];
		$type    = ! $node->parent ? 'row' : null;
		$nodes   = self::get_nodes( $type, $node->parent );
		$new_pos = 0;

		// Make sure node positions start at zero.
		foreach ( $nodes as $node ) {
			$data[ $node->node ]->position = $new_pos;
			$new_pos++;
		}

		// Get the node and remove it from the array.
		$node    = $data[ $node_id ];
		$removed = array_splice( $nodes, $node->position, 1 );
		$new_pos = 0;

		// Reposition it in the array.
		array_splice( $nodes, $position, 0, $removed );

		// Update the position data.
		$updated_nodes = array();
		foreach ( $nodes as $node ) {
			$data[ $node->node ]->position = $new_pos;

			// Get node fragments for redux
			$updated_nodes[ $node->node ]           = new StdClass();
			$updated_nodes[ $node->node ]->position = $new_pos;

			$new_pos++;
		}

		// Update the layout data.
		self::update_layout_data( $data );

		return array(
			'nodeId'       => $node_id,
			'nodeType'     => $data[ $node_id ]->type,
			'moduleType'   => 'module' === $data[ $node_id ]->type ? $data[ $node_id ]->settings->type : null,
			'parent'       => $node->parent,
			'updatedNodes' => $updated_nodes,
		);
	}

	/**
	 * Moves a node to another parent.
	 *
	 * @since 1.0
	 * @param string $node_id ID of the node to move.
	 * @param int $new_parent_id ID of the new parent.
	 * @param int $position The position in the new parent.
	 * @return void
	 */
	static public function move_node( $node_id = null, $new_parent_id = null, $position = 0 ) {
		$data          = self::get_layout_data();
		$new_parent_id = ! $new_parent_id ? null : $new_parent_id;
		$new_parent    = self::get_node( $new_parent_id );
		$node          = self::get_node( $node_id );
		$siblings      = self::get_nodes( null, $node->parent );
		$sibling_pos   = 0;

		// Set the node's new parent.
		$data[ $node_id ]->parent = $new_parent ? $new_parent->node : null;

		// Remove the node from the $siblings array.
		unset( $siblings[ $node_id ] );

		// Reorder old siblings.
		foreach ( $siblings as $sibling ) {
			$data[ $sibling->node ]->position = $sibling_pos;
			$sibling_pos++;
		}

		// Update the layout data.
		self::update_layout_data( $data );

		// Set the node's new order.
		$reordered = self::reorder_node( $node_id, $position );

		// Get updated node fragments for redux
		$updated                       = $reordered['updatedNodes'];
		$updated[ $node_id ]           = new StdClass();
		$updated[ $node_id ]->position = intval( $position );
		$updated[ $node_id ]->parent   = $new_parent_id;

		return array(
			'nodeId'       => $node_id,
			'nodeType'     => $node->type,
			'moduleType'   => 'module' === $node->type ? $node->settings->type : null,
			'parent'       => $new_parent_id,
			'updatedNodes' => $updated,
		);
	}

	/**
	 * Adds a row to the current layout.
	 *
	 * @since 1.0
	 * @param string $cols The type of column layout to use or the node ID of a col or group to move into this row.
	 * @param int $position The position of the new row.
	 * @param string $module Optional. The node ID of an existing module to move to this row.
	 * @return object The new row object.
	 */
	static public function add_row( $cols = '1-col', $position = false, $module = null ) {
		$data        = self::get_layout_data();
		$settings    = self::get_row_defaults();
		$row_node_id = self::generate_node_id();

		// Add the row.
		$data[ $row_node_id ]           = new StdClass();
		$data[ $row_node_id ]->node     = $row_node_id;
		$data[ $row_node_id ]->type     = 'row';
		$data[ $row_node_id ]->parent   = null;
		$data[ $row_node_id ]->position = self::next_node_position( 'row' );
		$data[ $row_node_id ]->settings = $settings;

		// Update the layout data.
		self::update_layout_data( $data );

		// Position the row.
		if ( false !== $position ) {
			self::reorder_node( $row_node_id, $position );
		}

		// Use an existing column group or add a new one?
		if ( isset( $data[ $cols ] ) && 'column-group' === $data[ $cols ]->type ) {
			$group = $data[ $cols ];
			self::move_node( $group->node, $row_node_id, 0 );
			$group = self::get_node( $group->node );
		} else {
			$group = $cols ? self::add_col_group( $row_node_id, $cols, 0 ) : null;
		}

		// Move an existing module to the row.
		if ( $module ) {
			if ( $group ) {
				$cols = self::get_nodes( 'column', $group->node );
				$col  = array_shift( $cols );
				self::move_node( $module, $col->node, 0 );
			} else {
				self::move_node( $module, $row_node_id, 0 );
			}
		}

		// Return the updated row.
		return self::get_node( $row_node_id );
	}

	/**
	 * Copies a row and adds it to the current layout.
	 *
	 * @since 1.0
	 * @param string $node_id Node ID of the row to copy.
	 * @param object $settings These settings will be used for the copy if present.
	 * @param string $settings_id The ID of the node who's settings were passed.
	 * @param bool $preserve_globals Whether to preserve global nodes.
	 * @return void
	 */
	static public function copy_row( $node_id = null, $settings = null, $settings_id = null, $preserve_globals = true ) {

		// Duplicate as a global row?
		$node = self::get_node( $node_id );
		if ( $preserve_globals && self::is_node_template_root( $node ) ) {
			return self::duplicate_global_node( $node->node );
		}

		// Duplicate as a regular row...
		$layout_data   = self::get_layout_data();
		$row           = self::get_node( $node_id );
		$new_row_id    = self::generate_node_id();
		$children      = self::get_nodes( null, $row );
		$new_nodes     = array();
		$template_cols = array();

		// Add the new row.
		$layout_data[ $new_row_id ]           = clone $row;
		$layout_data[ $new_row_id ]->settings = clone $row->settings;
		$layout_data[ $new_row_id ]->node     = $new_row_id;

		// Unset row template data.
		if ( isset( $layout_data[ $new_row_id ]->template_id ) ) {
			unset( $layout_data[ $new_row_id ]->template_id );
			unset( $layout_data[ $new_row_id ]->template_node_id );
			unset( $layout_data[ $new_row_id ]->template_root_node );
			unset( $layout_data[ $new_row_id ]->settings->dynamic_fields );
			unset( $layout_data[ $new_row_id ]->settings->dynamic_node_settings );
		}

		// Preliminary layout data update so the new row is available.
		self::update_layout_data( $layout_data );

		// Get the new child nodes.
		foreach ( $children as $child ) {

			// Handle modules as direct row children.
			if ( 'module' === $child->type ) {
				$child_copy = self::copy_module( $child, $child->settings, $new_row_id, null, $preserve_globals );
				if ( $child_copy ) {
					$new_nodes[ $child_copy->node ] = $child_copy;
					if ( ! $preserve_globals || ! self::is_node_global( $child_copy ) ) {
						$child_copy_children = self::get_nested_nodes( $child_copy->node );
						$new_nodes           = array_merge( $new_nodes, $child_copy_children );
					}
				}
				continue;
			}

			// Handle column groups as direct row children.
			$new_nodes[ $child->node ] = clone $child;
			$cols                      = self::get_nodes( 'column', $child );

			foreach ( $cols as $col ) {

				$new_nodes[ $col->node ]           = clone $col;
				$new_nodes[ $col->node ]->settings = clone $col->settings;
				$nodes                             = self::get_nodes( null, $col );

				foreach ( $nodes as $node ) {

					if ( 'module' == $node->type ) {
						$module_copy = self::copy_module( $node, $node->settings, $node->parent, $node->position, $preserve_globals );
						if ( $module_copy ) {
							$new_nodes[ $module_copy->node ] = $module_copy;
							if ( ! $preserve_globals || ! self::is_node_global( $module_copy ) ) {
								$module_copy_children = self::get_nested_nodes( $module_copy->node );
								$new_nodes            = array_merge( $new_nodes, $module_copy_children );
							}
						}
					} elseif ( 'column-group' == $node->type ) {

						$new_nodes[ $node->node ] = clone $node;
						$nested_cols              = self::get_nodes( 'column', $node );

						foreach ( $nested_cols as $nested_col ) {

							$new_nodes[ $nested_col->node ]           = clone $nested_col;
							$new_nodes[ $nested_col->node ]->settings = clone $nested_col->settings;
							$modules                                  = self::get_nodes( 'module', $nested_col );

							foreach ( $modules as $module ) {
								$module_copy = self::copy_module( $module, $module->settings, $module->parent, $module->position, $preserve_globals );
								if ( $module_copy ) {
									$new_nodes[ $module_copy->node ] = $module_copy;
									if ( ! $preserve_globals || ! self::is_node_global( $module_copy ) ) {
										$module_copy_children = self::get_nested_nodes( $module_copy->node );
										$new_nodes            = array_merge( $new_nodes, $module_copy_children );
									}
								}
							}
						}
					}
				}
			}
		}

		// Apply settings that were passed if we have them.
		if ( $settings && $settings_id ) {
			if ( $settings_id === $row->node ) {
				$layout_data[ $new_row_id ]->settings = (object) array_merge( (array) $row->settings, (array) $settings );
			} elseif ( isset( $new_nodes[ $settings_id ] ) ) {
				$new_nodes[ $settings_id ]->settings = (object) array_merge( (array) $new_nodes[ $settings_id ]->settings, (array) $settings );
			}
		}

		// Generate new child ids.
		$new_nodes = self::generate_new_node_ids( $new_nodes );

		// Set col group parent ids to the new row id and unset template data.
		foreach ( $new_nodes as $child_node_id => $child ) {

			// Check for column template's new node id.
			if ( isset( $child->template_node_id ) ) {
				$template_cols[ $child->template_node_id ] = $child_node_id;
			}

			if ( 'column-group' == $child->type ) {
				if ( $child->parent == $row->node || ( isset( $row->template_node_id ) && $child->parent == $row->template_node_id ) ) {
					$new_nodes[ $child_node_id ]->parent = $new_row_id;
				}
			} elseif ( 'module' == $child->type ) {
				if ( isset( $template_cols[ $child->parent ] ) ) {
					$new_nodes[ $child_node_id ]->parent = $template_cols[ $child->parent ];
				}
			}

			// Unset template data.
			if ( ! $preserve_globals && isset( $new_nodes[ $child_node_id ]->template_id ) ) {
				unset( $new_nodes[ $child_node_id ]->template_id );
				unset( $new_nodes[ $child_node_id ]->template_node_id );
				unset( $new_nodes[ $child_node_id ]->template_root_node );
				unset( $new_nodes[ $child_node_id ]->settings->dynamic_fields );
				unset( $new_nodes[ $child_node_id ]->settings->dynamic_node_settings );
			}
		}

		// Merge the child data.
		$layout_data = array_merge( $layout_data, $new_nodes );

		// Update the layout data.
		self::update_layout_data( $layout_data );

		// Position the new row.
		self::reorder_node( $new_row_id, $row->position + 1 );

		// Return the new row.
		return self::get_node( $new_row_id );
	}

	/**
	 * Returns the default settings for row nodes.
	 *
	 * @since 1.0
	 * @return object
	 */
	static public function get_row_defaults() {
		$settings = self::get_settings_form_defaults( 'row' );
		$settings = self::merge_nested_form_defaults( 'general', 'row', $settings );
		return $settings;
	}

	/**
	 * Runs row specific logic on new row settings.
	 *
	 * @since 1.0
	 * @param object $row A row node.
	 * @param object $new_settings The new settings object.
	 * @return object
	 */
	static public function process_row_settings( $row, $new_settings ) {
		// Cache background video data.
		if ( 'video' == $new_settings->bg_type ) {

			// Video Fallback Photo
			if ( ! empty( $new_settings->bg_video_fallback_src ) ) {
				$fallback = $new_settings->bg_video_fallback_src;
			} else {
				$fallback = '';
			}

			if ( 'wordpress' == $new_settings->bg_video_source ) {
				// Video MP4
				$mp4 = FLBuilderPhoto::get_attachment_data( $new_settings->bg_video );

				if ( $mp4 ) {
					$parts                                 = explode( '.', $mp4->filename );
					$mp4->extension                        = array_pop( $parts );
					$new_settings->bg_video_data           = $mp4;
					$new_settings->bg_video_data->fallback = $fallback;
				} else {
					$new_settings->bg_video_data            = new stdClass();
					$new_settings->bg_video_data->url       = '';
					$new_settings->bg_video_data->width     = '';
					$new_settings->bg_video_data->height    = '';
					$new_settings->bg_video_data->extension = '';
					$new_settings->bg_video_data->fallback  = '';
				}

				// Video WebM
				$webm = FLBuilderPhoto::get_attachment_data( $new_settings->bg_video_webm );

				if ( $webm ) {
					$parts                                      = explode( '.', $webm->filename );
					$webm->extension                            = array_pop( $parts );
					$new_settings->bg_video_webm_data           = $webm;
					$new_settings->bg_video_webm_data->fallback = $fallback;
				} else {
					$new_settings->bg_video_webm_data            = new stdClass();
					$new_settings->bg_video_webm_data->url       = '';
					$new_settings->bg_video_webm_data->width     = '';
					$new_settings->bg_video_webm_data->height    = '';
					$new_settings->bg_video_webm_data->extension = '';
					$new_settings->bg_video_webm_data->fallback  = '';
				}
			}
		}

		// Cache background slideshow data.
		if ( 'slideshow' == $new_settings->bg_type && 'wordpress' == $new_settings->ss_source && class_exists( 'FLSlideshowModule' ) ) {

			// Make sure we have a photo data object.
			if ( ! isset( $row->settings->ss_photo_data ) ) {
				$row->settings->ss_photo_data = new StdClass();
			}

			// Hijack the slideshow module to get WordPress photo data.
			$ss                          = new FLSlideshowModule();
			$ss->settings                = new StdClass();
			$ss->settings->photos        = $new_settings->ss_photos;
			$ss->settings->photo_data    = $row->settings->ss_photo_data;
			$new_settings->ss_photo_data = $ss->get_wordpress_photos();
		}

		return $new_settings;
	}


	/**
	 * Returns background data for a row.
	 *
	 * @since 1.0
	 * @param object $row A row node.
	 * @return object
	 */
	static public function get_row_bg_data( $row ) {
		$data = null;

		// Background Video
		if ( 'video' == $row->settings->bg_type ) {

			if ( isset( $row->settings->bg_video_data ) ) {
				$data        = array();
				$data['mp4'] = $row->settings->bg_video_data;
			}
			if ( isset( $row->settings->bg_video_webm_data ) ) {

				if ( ! $data ) {
					$data = array();
				}

				$data['webm'] = $row->settings->bg_video_webm_data;
			}
		} elseif ( 'slideshow' == $row->settings->bg_type && isset( $row->settings->ss_photo_data ) ) {
			$data = $row->settings->ss_photo_data;
		}

		return $data;
	}

	/**
	 * Returns the source for a row background slideshow.
	 *
	 * @since 1.0
	 * @param object $row A row node.
	 * @return string
	 */
	static public function get_row_slideshow_source( $row ) {
		// Make sure we have a photo data object.
		if ( ! isset( $row->settings->ss_photo_data ) ) {
			$row->settings->ss_photo_data = new StdClass();
		}

		// This class does not exist in Lite version.
		if ( ! class_exists( 'FLSlideshowModule' ) ) {
			return false;
		}

		// Hijack the slideshow module to get the source.
		$ss                       = new FLSlideshowModule();
		$ss->settings             = new StdClass();
		$ss->settings->source     = $row->settings->ss_source;
		$ss->settings->photos     = $row->settings->ss_photos;
		$ss->settings->feed_url   = $row->settings->ss_feed_url;
		$ss->settings->photo_data = $row->settings->ss_photo_data;

		// Return the slideshow source.
		return $ss->get_source();
	}

	/**
	 * Set the max-width of a specific row.
	 *
	 * @since 2.0
	 * @param int Row node id
	 * @param int Width
	 * @return void
	 */
	static public function resize_row_content( $node_id, $width ) {
		$data                             = self::get_layout_data();
		$row                              = self::get_node( $node_id );
		$row->settings->max_content_width = $width;
		$data[ $node_id ]                 = $row;
		self::update_layout_data( $data );
	}

	/**
	 * Adds a column group to a row in the current layout.
	 *
	 * @since 1.0
	 * @param string $node_id A row node ID.
	 * @param string $cols The type of column group layout or the ID of an existing column to add.
	 * @param int $position The position of the new column group.
	 * @param string $module Optional. The node ID of an existing module to move to this group.
	 * @return object The new column group object.
	 */
	static public function add_col_group( $node_id = null, $cols = '1-col', $position = false, $module = null ) {
		$data          = self::get_layout_data();
		$group_node_id = self::generate_node_id();
		$parent        = self::get_node( $node_id );
		$old_group     = null;

		// Add the column group.
		$data[ $group_node_id ]           = new StdClass();
		$data[ $group_node_id ]->node     = $group_node_id;
		$data[ $group_node_id ]->type     = 'column-group';
		$data[ $group_node_id ]->parent   = $node_id;
		$data[ $group_node_id ]->position = self::next_node_position( null, $node_id );
		$data[ $group_node_id ]->settings = '';

		// Add node template data.
		if ( self::is_node_global( $parent ) ) {
			$data[ $group_node_id ]->template_id      = $parent->template_id;
			$data[ $group_node_id ]->template_node_id = $group_node_id;
		}

		// Add new columns?
		if ( isset( self::$row_layouts[ $cols ] ) ) {

			for ( $i = 0; $i < count( self::$row_layouts[ $cols ] ); $i++ ) {

				$col_node_id                          = self::generate_node_id();
				$data[ $col_node_id ]                 = new StdClass();
				$data[ $col_node_id ]->node           = $col_node_id;
				$data[ $col_node_id ]->type           = 'column';
				$data[ $col_node_id ]->parent         = $group_node_id;
				$data[ $col_node_id ]->position       = $i;
				$data[ $col_node_id ]->settings       = new StdClass();
				$data[ $col_node_id ]->settings->size = self::$row_layouts[ $cols ][ $i ];

				if ( self::is_node_global( $parent ) ) {
					$data[ $col_node_id ]->template_id      = $parent->template_id;
					$data[ $col_node_id ]->template_node_id = $col_node_id;
				}
			}
		} elseif ( isset( $data[ $cols ] ) ) {

			$old_group   = $data[ $cols ]->parent;
			$siblings    = self::get_nodes( 'column', $old_group );
			$sibling_pos = 0;

			// Add the column to the group.
			$data[ $cols ]->parent         = $group_node_id;
			$data[ $cols ]->position       = 0;
			$data[ $cols ]->settings->size = 100;

			if ( self::is_node_global( $parent ) ) {
				$data[ $cols ]->template_id      = $parent->template_id;
				$data[ $cols ]->template_node_id = $data[ $cols ]->node;
			}

			// Remove the column from the $siblings array.
			unset( $siblings[ $cols ] );

			// Reorder old siblings.
			foreach ( $siblings as $sibling ) {
				$data[ $sibling->node ]->position = $sibling_pos;
				$sibling_pos++;
			}
		}

		// Update the layout data.
		self::update_layout_data( $data );

		// Delete an existing column's old group if empty or resize it.
		if ( $old_group ) {
			if ( 0 === count( self::get_nodes( 'column', $old_group ) ) ) {
				self::delete_node( $old_group );
			} else {
				self::reset_col_widths( $old_group );
			}
		}

		// Position the column group.
		if ( false !== $position ) {
			self::reorder_node( $group_node_id, $position );
		}

		// Move an existing module to the group.
		if ( $module ) {
			$cols = self::get_nodes( 'column', $group_node_id );
			$col  = array_shift( $cols );
			self::move_node( $module, $col->node, 0 );
		}

		// Return the column group.
		return self::get_node( $group_node_id );
	}

	/**
	 * Runs column specific logic on new column settings.
	 *
	 * @since 1.0
	 * @param object $col A column node.
	 * @param object $new_settings The new settings object.
	 * @return object
	 */
	static public function process_col_settings( $col, $new_settings ) {
		$post_data = self::get_post_data();

		// Don't process for preview nodes or if column is parent.
		if ( isset( $post_data['node_preview'] ) || empty( $col->parent ) ) {
			return $new_settings;
		}

		// Resize sibling cols if needed.
		$new_settings->size = self::resize_col( $col->node, $new_settings->size );

		// Update other sibling vars as needed.
		$equal_height      = false;
		$content_alignment = false;
		$responsive_order  = false;

		// Adjust sibling equal height?
		if ( $col->settings->equal_height != $new_settings->equal_height ) {
			$equal_height = $new_settings->equal_height;
		}

		// Adjust sibling content alignment?
		if ( $col->settings->content_alignment != $new_settings->content_alignment ) {
			$content_alignment = $new_settings->content_alignment;
		}

		// Adjust sibling responsive order?
		if ( $col->settings->responsive_order != $new_settings->responsive_order ) {
			$responsive_order = $new_settings->responsive_order;
		}

		// Update the siblings?
		if ( false !== $equal_height || false !== $content_alignment || false !== $responsive_order ) {

			$data = self::get_layout_data();
			$cols = self::get_nodes( 'column', $col->parent );

			foreach ( $cols as $node_id => $node ) {

				if ( false !== $equal_height ) {
					$data[ $node_id ]->settings->equal_height = $equal_height;
				}
				if ( false !== $content_alignment ) {
					$data[ $node_id ]->settings->content_alignment = $content_alignment;
				}
				if ( false !== $responsive_order ) {
					$data[ $node_id ]->settings->responsive_order = $responsive_order;
				}
			}

			self::update_layout_data( $data );
		}

		return $new_settings;
	}

	/**
	 * Deletes a column.
	 *
	 * @since 1.0
	 * @param string $node_id Node ID of the column to delete (can also be a group).
	 * @param int $new_width New width of the remaining columns.
	 * @return void
	 */
	static public function delete_col( $node_id = null, $new_width = 100 ) {
		$col = self::get_node( $node_id );

		// Delete the column.
		self::delete_node( $node_id );

		// Return if the node we just deleted was a group.
		if ( 'column-group' == $col->type ) {
			return;
		}

		// Get the group
		$group = self::get_node( $col->parent );

		// Get the group children.
		$cols = self::get_nodes( 'column', $group->node );

		// Delete the group if empty.
		if ( count( $cols ) === 0 ) {
			self::delete_node( $group->node );
		} else {

			// Get the layout data.
			$data = self::get_layout_data();

			// Loop through the columns.
			foreach ( $cols as $col_id => $col ) {

				// Set the new size.
				$data[ $col_id ]->settings->size = round( $new_width, 3 );
			}

			// Update the layout data.
			self::update_layout_data( $data );
		}
	}

	/**
	 * Moves a column within a group.
	 *
	 * @since 1.9
	 * @param string $node_id
	 * @param int $position
	 * @return void
	 */
	static public function reorder_col( $node_id, $position = 0 ) {
		$col = self::get_node( $node_id );

		self::reorder_node( $node_id, $position );
		self::reset_col_widths( $col->parent );
	}

	/**
	 * Moves a column from one group to another.
	 *
	 * @since 1.9
	 * @param string $col_id
	 * @param string $group_id
	 * @param int $position
	 * @param array $resize
	 * @return void
	 */
	static public function move_col( $col_id, $group_id, $position, $resize = array() ) {
		$col       = self::get_node( $col_id );
		$old_group = self::get_node( $col->parent );

		self::move_node( $col_id, $group_id, $position );

		if ( 0 === count( self::get_nodes( 'column', $old_group ) ) ) {
			self::delete_node( $old_group->node );
			self::reset_col_widths( $group_id );
		} else {
			self::reset_col_widths( $resize );
		}
	}

	/**
	 * Resizes a column.
	 *
	 * @since 1.0
	 * @param string $node_id Node ID of the column to resize.
	 * @param int $new_width New width of the column.
	 * @return int The new width
	 */
	static public function resize_col( $node_id = null, $new_width = 100 ) {
		$data           = self::get_layout_data();
		$col            = $data[ $node_id ];
		$group          = $data[ $col->parent ];
		$cols           = array_values( self::get_nodes( 'column', $group->node ) );
		$pos            = $col->position;
		$siblings       = array();
		$siblings_width = 0;
		$num_cols       = count( $cols );
		$min_width      = 8;
		$max_width      = 100 - $min_width;

		// Since version 2.5. Allow single column to be resized.
		if ( 1 == $num_cols || ! is_numeric( $new_width ) ) {
			return absint( $new_width );
		}

		// Find the sibling column to absorb this resize.
		for ( $i = 0; $i < count( $cols ); $i++ ) {
			if ( $col->node == $cols[ $i ]->node ) {
				if ( isset( $cols[ $i + 1 ] ) ) {
					$sibling = $cols[ $i + 1 ];
				} else {
					$sibling = $cols[ $i - 1 ];
				}
				break;
			}
		}

		// Find other siblings.
		foreach ( $cols as $c ) {

			if ( $col->node == $c->node ) {
				continue;
			}
			if ( $sibling->node == $c->node ) {
				continue;
			}

			$siblings[]      = $c;
			$max_width      -= $c->settings->size;
			$siblings_width += $c->settings->size;
		}

		// Make sure the new width isn't too small.
		if ( $new_width < $min_width ) {
			$new_width = $min_width;
		}

		// Make sure the new width isn't too big.
		if ( $new_width > $max_width ) {
			$new_width = $max_width;
		}

		// Save new sibling size.
		$data[ $sibling->node ]->settings->size = round( 100 - $siblings_width - $new_width, 3 );

		// Save new column size.
		$data[ $col->node ]->settings->size = $new_width;

		// Update the layout data.
		self::update_layout_data( $data );

		// Return the new size.
		return $new_width;
	}

	/**
	 * Resizes a column and its sibling using the provided widths.
	 *
	 * @since 1.6.4
	 * @param string $col_id Node ID of the column to resize.
	 * @param int $col_width New width of the column.
	 * @param string $sibling_id Node ID of the sibling to resize.
	 * @param int $sibling_width New width of the sibling.
	 * @return Array affected node fragments
	 */
	static public function resize_cols( $col_id = null, $col_width = null, $sibling_id = null, $sibling_width = null ) {
		$data = self::get_layout_data();

		// Save the column width.
		$data[ $col_id ]->settings->size = $col_width;

		// Save the sibling width.
		$data[ $sibling_id ]->settings->size = $sibling_width;

		// Update the layout data.
		self::update_layout_data( $data );

		// Return node fragments for redux
		$updated_nodes = array(
			$col_id     => array(
				'settings' => array(
					'size' => $col_width,
				),
			),
			$sibling_id => array(
				'settings' => array(
					'size' => $sibling_width,
				),
			),
		);

		// Passed in array for consistency with other responses
		return array( 'updatedNodes' => $updated_nodes );
	}

	/**
	 * Resets the widths of all columns in a group.
	 *
	 * @since 1.6.4
	 * @param string|array $group_id Node ID of the group whose columns to reset or an array of group IDs.
	 * @return void
	 */
	static public function reset_col_widths( $group_id = null ) {

		if ( ! $group_id ) {
			return;
		}

		if ( 'array' == gettype( $group_id ) ) {
			foreach ( $group_id as $id ) {
				self::reset_col_widths( $id );
			}
			return;
		}

		$data      = self::get_layout_data();
		$post_data = self::get_post_data();
		$cols      = self::get_nodes( 'column', $group_id );
		$width     = round( 100 / count( $cols ), 3 );

		foreach ( $cols as $col_id => $col ) {
			$data[ $col_id ]->settings->size = $width;
		}

		self::update_layout_data( $data );
	}

	/**
	 * Adds a column to a column group in the current layout.
	 *
	 * @since 1.9
	 * @param string $node_id A column group node ID.
	 * @param int $position The position of the new column.
	 * @return object The new column object.
	 */
	static public function add_col( $node_id = null, $position = false ) {
		$group    = self::get_node( $node_id );
		$cols     = self::get_nodes( 'column', $group );
		$num_cols = count( $cols );
		$i        = 0;
		$sibling  = false;
		$insert   = 'before';

		foreach ( $cols as $col ) {
			if ( $i == $position ) {
				$sibling = $col;
				break;
			}
			$i++;
		}

		if ( ! $sibling ) {
			$sibling = $col;
			$insert  = 'after';
		}

		self::add_cols( $sibling->node, $insert );

		$cols    = self::get_nodes( 'column', $group );
		$col_ids = array_keys( $cols );

		return $cols[ $col_ids[ $position ] ];
	}

	/**
	 * Inserts a column (or columns) before or after another column.
	 *
	 * @since 1.6.4
	 * @param string $node_id Node ID of the column to insert before or after.
	 * @param string $insert Either before or after.
	 * @param string $type The type of column(s) to insert.
	 * @param boolean $nested Whether these columns are nested or not.
	 * @param string $module Optional. The node ID of an existing module to move to this group.
	 * @return object
	 */
	static public function add_cols( $col_id, $insert = 'before', $type = '1-col', $nested = false, $module = null ) {
		$data         = self::get_layout_data();
		$col          = self::get_node( $col_id );
		$parent       = self::get_node( $col->parent );
		$cols         = self::get_nodes( 'column', $col->parent );
		$global       = self::is_node_global( $parent );
		$num_new_cols = count( self::$row_layouts[ $type ] );
		$num_cols     = count( $cols );
		$max_cols     = $nested ? 4 : 12;
		$reposition   = false;
		$position     = 0;

		// Make sure we have 12 columns or less.
		if ( $num_cols + $num_new_cols > $max_cols ) {
			$num_new_cols = $num_new_cols - ( $num_cols + $num_new_cols - $max_cols );
			$num_cols     = $max_cols;
		} else {
			$num_cols += $num_new_cols;
		}

		// Get the new width.
		if ( 6 === $num_cols ) {
			$new_width = 16.65;
		} elseif ( 7 === $num_cols ) {
			$new_width = 14.28;
		} else {
			$new_width = round( 100 / $num_cols, 3 );
		}

		// Get the new column position.
		if ( 'before' == $insert ) {
			$new_col_position = $col->position - 1 < 0 ? 0 : $col->position;
		} else {
			$new_col_position = $col->position + 1;
		}

		// Add the new columns.
		for ( $i = 0; $i < $num_new_cols; $i++ ) {

			$new_col_id                          = self::generate_node_id();
			$data[ $new_col_id ]                 = new StdClass();
			$data[ $new_col_id ]->node           = $new_col_id;
			$data[ $new_col_id ]->type           = 'column';
			$data[ $new_col_id ]->parent         = $parent->node;
			$data[ $new_col_id ]->position       = $new_col_position;
			$data[ $new_col_id ]->settings       = new StdClass();
			$data[ $new_col_id ]->settings->size = $new_width;

			// Add node template data.
			if ( $global ) {
				$data[ $new_col_id ]->template_id      = $parent->template_id;
				$data[ $new_col_id ]->template_node_id = $new_col_id;
			}

			$new_col_position++;
		}

		// Resize sibling columns and set their new position.
		foreach ( $cols as $sibling_col_id => $sibling_col ) {

			$data[ $sibling_col_id ]->settings->size = $new_width;

			if ( $sibling_col_id == $col_id ) {

				$reposition = true;

				if ( 'before' == $insert ) {
					$data[ $sibling_col_id ]->position = $new_col_position;
					$new_col_position++;
				}
			} elseif ( $reposition ) {
				$data[ $sibling_col_id ]->position = $new_col_position;
				$new_col_position++;
			} else {
				$data[ $sibling_col_id ]->position = $position;
				$position++;
			}
		}

		// Update the layout data.
		self::update_layout_data( $data );

		// Move an existing module to the group.
		if ( $module ) {
			self::move_node( $module, $new_col_id, 0 );
		}

		// Return the column group.
		return $parent;
	}

	/**
	 * Adds a parent node for a column if a parent with the supplied
	 * parent ID doesn't exist.
	 *
	 * @since 2.1
	 * @param string $parent_id The node ID of the parent to look for.
	 * @param int $position The position of the parent.
	 * @return string|null The new parent ID or null if none exists.
	 */
	static public function add_col_parent( $parent_id = null, $position = null ) {
		$data   = self::get_layout_data();
		$parent = ! $parent_id ? null : self::get_node( $parent_id );

		if ( ! $parent ) {
			// Add a new row if we don't have a parent, but don't add column.
			$row        = self::add_row( '0-cols', $position );
			$col_groups = self::get_nodes( 'column-group', $row->node );
			$col_group  = array_shift( $col_groups );
			$parent_id  = $col_group->node;
		} elseif ( 'row' == $parent->type ) {
			// Add a new column group if the parent is a row, but don't add column.
			$col_group = self::add_col_group( $parent->node, null, $position );
			$parent_id = $col_group->node;
		}

		return $parent_id;
	}

	/**
	 * Returns a column's parent node of the specified type.
	 *
	 * @since 2.1
	 * @param string $type The type of parent to return.
	 * @param string|object $column_id The columns's node ID. Can also be a column object.
	 * @return object The parent node.
	 */
	static public function get_col_parent( $type, $column_id ) {
		$column = is_object( $column_id ) ? $column_id : self::get_node( $column_id );
		$nodes  = self::get_categorized_nodes();

		foreach ( $nodes['groups'] as $group ) {

			if ( $group->node == $column->parent ) {

				if ( 'column-group' == $type ) {
					return $group;
				}

				foreach ( $nodes['rows'] as $row ) {

					if ( $row->node == $group->parent ) {
						return $row;
					}
				}
			}
		}

		return null;
	}

	/**
	 * Copies a column and adds it to the current layout.
	 *
	 * @since 2.0
	 * @param string $node_id Node ID of the column to copy.
	 * @param object $settings These settings will be used for the copy if present.
	 * @param string $settings_id The ID of the node who's settings were passed.
	 * @param boolean $preserve_globals Whether to preserve global nodes.
	 * @return void
	 */
	static public function copy_col( $node_id = null, $settings = null, $settings_id = null, $preserve_globals = true ) {

		// Duplicate as a global column?
		$node = self::get_node( $node_id );
		if ( $preserve_globals && self::is_node_template_root( $node ) ) {
			return self::duplicate_global_node( $node->node );
		}

		// Duplicate as a regular column...
		$layout_data = self::get_layout_data();
		$col         = self::get_node( $node_id );
		$new_col_id  = self::generate_node_id();
		$nodes       = self::get_nodes( null, $col );
		$parent      = self::get_node_parent( $node_id );
		$new_nodes   = array();

		// Add the new column.
		$layout_data[ $new_col_id ]           = clone $col;
		$layout_data[ $new_col_id ]->settings = clone $col->settings;
		$layout_data[ $new_col_id ]->node     = $new_col_id;

		// Unset column template data.
		if ( isset( $layout_data[ $new_col_id ]->template_id ) ) {

			// Get the column root parent on a page.
			if ( isset( $layout_data[ $new_col_id ]->template_root_node ) ) {
				$parent = self::get_node( $layout_data[ $new_col_id ]->parent );
			}

			// Check if parent is a global node.
			if ( self::is_node_global( $parent ) ) {
				$layout_data[ $new_col_id ]->template_id      = $parent->template_id;
				$layout_data[ $new_col_id ]->template_node_id = $new_col_id;
			} else {
				unset( $layout_data[ $new_col_id ]->template_id );
				unset( $layout_data[ $new_col_id ]->template_node_id );
				unset( $layout_data[ $new_col_id ]->settings->dynamic_fields );
				unset( $layout_data[ $new_col_id ]->settings->dynamic_node_settings );

			}
			unset( $layout_data[ $new_col_id ]->template_root_node );
		}

		// Get the new child nodes.
		foreach ( $nodes as $node ) {

			if ( 'module' == $node->type ) {
				$module_copy = self::copy_module( $node, $node->settings, $node->parent, $node->position, $preserve_globals );
				if ( $module_copy ) {
					$new_nodes[ $module_copy->node ] = $module_copy;
					if ( ! $preserve_globals || ! self::is_node_global( $module_copy ) ) {
						$module_copy_children = self::get_nested_nodes( $module_copy->node );
						$new_nodes            = array_merge( $new_nodes, $module_copy_children );
					}
				}
			} elseif ( 'column-group' == $node->type ) {

				$new_nodes[ $node->node ] = clone $node;
				$nested_cols              = self::get_nodes( 'column', $node );

				foreach ( $nested_cols as $nested_col ) {

					$new_nodes[ $nested_col->node ]           = clone $nested_col;
					$new_nodes[ $nested_col->node ]->settings = clone $nested_col->settings;
					$modules                                  = self::get_nodes( 'module', $nested_col );

					foreach ( $modules as $module ) {
						$new_nodes[ $module->node ]           = clone $module;
						$new_nodes[ $module->node ]->settings = self::clone_module_settings( $module->settings );
					}
				}
			}
		}

		// Apply settings that were passed if we have them.
		if ( $settings && $settings_id ) {
			if ( $settings_id === $col->node ) {
				$layout_data[ $new_col_id ]->settings = (object) array_merge( (array) $col->settings, (array) $settings );
			} elseif ( isset( $new_nodes[ $settings_id ] ) ) {
				$new_nodes[ $settings_id ]->settings = (object) array_merge( (array) $new_nodes[ $settings_id ]->settings, (array) $settings );
			}
		}

		// Generate new child ids.
		$new_nodes = self::generate_new_node_ids( $new_nodes );

		// Set child parent ids to the new column id and unset template data.
		foreach ( $new_nodes as $child_node_id => $child ) {
			if ( $child->parent == $col->node || ( isset( $col->template_node_id ) && $child->parent == $col->template_node_id ) ) {
				$new_nodes[ $child_node_id ]->parent = $new_col_id;
			}
			if ( ! $preserve_globals && isset( $new_nodes[ $child_node_id ]->template_id ) ) {
				// Check if the column is global.
				if ( isset( $layout_data[ $new_col_id ]->template_node_id ) ) {
					$new_nodes[ $child_node_id ]->template_id      = $parent->template_id;
					$new_nodes[ $child_node_id ]->template_node_id = $child_node_id;
				} else {
					unset( $new_nodes[ $child_node_id ]->template_id );
					unset( $new_nodes[ $child_node_id ]->template_node_id );
				}
			}
		}

		// Merge the child data.
		$layout_data = array_merge( $layout_data, $new_nodes );

		// Update the layout data.
		self::update_layout_data( $layout_data );

		// Position the new column.
		self::reorder_node( $new_col_id, $col->position + 1 );

		// Reset the column widths.
		self::reset_col_widths( $col->parent );

		// Return the new column.
		return self::get_node( $new_col_id );
	}

	/**
	 * Returns the default settings for column nodes.
	 *
	 * @since 1.0
	 * @return object
	 */
	static public function get_col_defaults() {
		$settings = self::get_settings_form_defaults( 'col' );
		$settings = self::merge_nested_form_defaults( 'general', 'col', $settings );
		return $settings;
	}

	/**
	 * Loads the classes for core builder modules.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function load_modules() {
		$paths = glob( FL_BUILDER_DIR . 'modules/*' );
		/**
		 * Filter the modules paths.
		 * @see fl_builder_load_modules_paths
		 */
		$paths       = apply_filters( 'fl_builder_load_modules_paths', $paths );
		$module_path = '';

		// Make sure we have an array.
		if ( ! is_array( $paths ) ) {
			return;
		}

		// Load all found modules.
		foreach ( $paths as $path ) {

			// Make sure we have a directory.
			if ( ! is_dir( $path ) ) {
				continue;
			}

			// Get the module slug.
			$slug = basename( $path );

			// Paths to check.
			$module_path  = $slug . '/' . $slug . '.php';
			$child_path   = get_stylesheet_directory() . '/fl-builder/modules/' . $module_path;
			$theme_path   = get_template_directory() . '/fl-builder/modules/' . $module_path;
			$builder_path = FL_BUILDER_DIR . 'modules/' . $module_path;

			// Check for the module class in a child theme.
			if ( is_child_theme() && file_exists( $child_path ) ) {
				require_once $child_path;
			} elseif ( file_exists( $theme_path ) ) {
				require_once $theme_path;
			} elseif ( file_exists( $builder_path ) ) {
				require_once $builder_path;
			}
		}
		/**
		 * After modules are included.
		 * @see fl_builder_register_extensions
		 */
		do_action( 'fl_builder_register_extensions' );
	}

	/**
	 * Registers a module with the builder.
	 *
	 * @since 1.0
	 * @param string $class The module's PHP class name.
	 * @param array $form The module's settings form.
	 * @return void
	 */
	static public function register_module( $class, $form ) {
		if ( ! class_exists( $class ) ) {
			return;
		}

		// Create a new instance of the module.
		$instance = new $class();

		// Log an error if a module with this slug already exists.
		if ( isset( self::$modules[ $instance->slug ] ) ) {
			/* translators: %s: module filename */
			error_log( sprintf( _x( 'A module with the filename %s.php already exists! Please namespace your module filenames to ensure compatibility with Beaver Builder.', '%s stands for the module filename', 'fl-builder' ), $instance->slug ) );
			return;
		}

		/**
		 * Use this filter to override the modules that are enabled in the builder.
		 * @see fl_builder_register_module
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		$instance->enabled = apply_filters( 'fl_builder_register_module', $instance->enabled, $instance );

		// Save the instance in the modules array.
		self::$modules[ $instance->slug ]       = $instance;
		self::$modules[ $instance->slug ]->form = apply_filters( 'fl_builder_register_settings_form', $form, $instance->slug );

		// Allow auto_style tab
		if ( false !== $instance->auto_style ) {
			self::$modules[ $instance->slug ]->form['auto_style'] = self::$settings_forms['auto_style'];
		}

		// Add Advanced tab last
		self::$modules[ $instance->slug ]->form['advanced'] = self::$settings_forms['module_advanced'];

		/**
		 * Use this filter to modify the config array for a settings form when it is registered.
		 * @see fl_builder_register_module_settings_form
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		self::$modules[ $instance->slug ]->form = apply_filters( 'fl_builder_register_module_settings_form', self::$modules[ $instance->slug ]->form, $instance->slug );

		// Load deprecations
		if ( file_exists( $instance->dir . 'deprecated.php' ) ) {
			require_once $instance->dir . 'deprecated.php';
		}
	}

	/**
	 * Registers an alias to a module with its own name,
	 * category and default settings.
	 *
	 * @since 1.10
	 * @param string $alias The alias key.
	 * @param array $config The alias config.
	 * @return void
	 */
	static public function register_module_alias( $alias, $config ) {
		if ( isset( self::$module_aliases[ $alias ] ) ) {
			/* translators: %s: module alias key */
			_doing_it_wrong( __CLASS__ . '::register_module_alias', sprintf( _x( 'The module alias %s already exists! Please namespace your module aliases to ensure compatibility with Beaver Builder.', '%s stands for the module alias key', 'fl-builder' ), $alias ), '1.10' );
			return;
		}
		if ( ! $config['module'] || ! isset( self::$modules[ $config['module'] ] ) ) {
			return;
		}
		if ( 'box' === $config['module'] ) {
			$enabled_modules = self::get_enabled_modules();
			if ( ! in_array( $config['module'], $enabled_modules ) || is_admin() ) {
				return;
			}
		}

		$module                = self::$modules[ $config['module'] ];
		$instance              = new stdClass();
		$instance->alias       = $alias;
		$instance->slug        = isset( $config['module'] ) ? $config['module'] : null;
		$instance->name        = isset( $config['name'] ) ? $config['name'] : $instance->slug;
		$instance->description = isset( $config['description'] ) ? $config['description'] : '';
		$instance->category    = isset( $config['category'] ) ? $config['category'] : null;
		$instance->group       = isset( $config['group'] ) ? $config['group'] : null;
		$instance->settings    = isset( $config['settings'] ) ? $config['settings'] : array();
		$instance->enabled     = isset( $config['enabled'] ) ? $config['enabled'] : true;
		$instance->icon        = isset( $config['icon'] ) ? $module->get_icon( $config['icon'] ) : FLBuilderModule::get_default_icon();
		$instance->location    = isset( $config['location'] ) ? $config['location'] : 'panel';

		// Container module support.
		$instance->accepts  = $module->accepts;
		$instance->parents  = $module->parents;
		$instance->template = isset( $config['template'] ) ? $config['template'] : [];

		self::$module_aliases[ $alias ] = $instance;
	}

	/**
	 * Returns a module alias if registered.
	 *
	 * @since 2.8
	 * @param string $alias The alias key.
	 * @return object|null
	 */
	static public function get_module_alias( $alias ) {
		if ( isset( self::$module_aliases[ $alias ] ) ) {
			return self::$module_aliases[ $alias ];
		}

		return null;
	}

	/**
	 * Returns the default settings for a module alias.
	 *
	 * @since 1.10
	 * @param string $alias The alias key.
	 * @return array|null
	 */
	static public function get_module_alias_settings( $alias ) {
		if ( isset( self::$module_aliases[ $alias ] ) ) {
			return self::$module_aliases[ $alias ]->settings;
		}

		return null;
	}

	/**
	 * Returns an array of module aliases that can be rendered inline
	 * and selected within container modules when they are empty.
	 *
	 * @since 2.9
	 * @return array
	 */
	static public function get_inline_module_aliases() {
		$aliases = [];

		foreach ( self::$module_aliases as $alias => $config ) {

			if ( ! $config->enabled || 'inline' !== $config->location ) {
				continue;
			}

			if ( ! isset( $aliases[ $config->slug ] ) ) {
				$aliases[ $config->slug ] = [];
			}

			$aliases[ $config->slug ][] = $config;
		}

		return $aliases;
	}

	/**
	 * Applies an inline module alias to an existing module.
	 * This is used to apply different child templates and
	 * settings to container modules.
	 *
	 * @since 2.9
	 * @param string $node_id
	 * @param string $alias
	 * @param object $form_settings
	 * @return array
	 */
	static public function apply_module_alias( $node_id, $alias, $form_settings = null ) {
		$config   = self::$module_aliases[ $alias ];
		$data     = self::get_layout_data();
		$settings = $data[ $node_id ]->settings;

		// Merge form settings passed from the frontend
		if ( $form_settings ) {
			$settings = (object) array_merge( (array) $settings, $form_settings );
		}

		// Merge alias settings
		$settings = (object) array_merge( (array) $settings, $config->settings );

		// Update the settings
		$data[ $node_id ]->settings = $settings;
		self::update_layout_data( $data );

		// Apply child template
		self::add_default_module_template( $node_id, $config->template );

		return array(
			'nodeId'   => $node_id,
			'settings' => $settings,
			'newNodes' => self::get_nested_nodes( $node_id ),
			'layout'   => FLBuilderAJAXLayout::render(),
		);
	}

	/**
	 * Checks to see if a module of a certain type has
	 * been registered.
	 *
	 * @since 1.9
	 * @param array $type The module's type slug.
	 * @return void
	 */
	static public function is_module_registered( $type ) {
		return isset( self::$modules[ $type ] );
	}

	/**
	 * Returns an array of modules that are enabled by default.
	 *
	 * @since 2.1
	 * @return array
	 */
	static public function get_default_enabled_modules() {
		$default = array_keys( self::$modules );

		$deprecated = self::get_deprecated_modules();

		// Remove deprecated modules from the defaults.
		foreach ( $default as $key => $slug ) {
			if ( in_array( $slug, $deprecated ) ) {
				unset( $default[ $key ] );
			}
		}

		return array_values( $default );
	}

	/**
	 * @since 2.4.1
	 */
	static public function get_deprecated_modules() {
		// These modules are deprecated and disabled by default.
		$deprecated = array(
			'social-buttons',
			'widget',
		);
		return $deprecated;
	}

	/**
	 * Returns an array of all modules that are enabled.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_enabled_modules() {
		$setting = self::get_admin_settings_option( '_fl_builder_enabled_modules', true );

		if ( ! $setting ) {
			// Fallback to the defaults if no saved setting.
			$setting = self::get_default_enabled_modules();
		} elseif ( in_array( 'all', $setting ) ) {
			// Redefine $setting in case new modules have been installed since the last save.
			$setting   = array_keys( self::$modules );
			$setting[] = 'all';
		}

		foreach ( self::$modules as $module_slug => $module ) {
			if ( ! $module->enabled && in_array( $module_slug, $setting ) ) {
				$key = array_search( $module_slug, $setting );
				unset( $setting[ $key ] );
			}
		}
		/**
		 * Array of enabled modules.
		 * @see fl_builder_enabled_modules
		 */
		return apply_filters( 'fl_builder_enabled_modules', $setting );
	}

	/**
	 * Returns an array of module group slugs and names.
	 *
	 * @since 2.0
	 * @return array
	 */
	static public function get_module_groups() {
		$groups = array();

		// Add module groups.
		foreach ( self::$modules as $module ) {

			if ( ! $module->group || ! $module->enabled ) {
				continue;
			}

			// Check if widgets are enabled
			if ( 'widget' == $module->slug && ! in_array( 'widget', self::get_enabled_modules() ) ) {
				continue;
			}

			$slug = sanitize_key( $module->group );

			if ( ! isset( $groups[ $slug ] ) ) {
				$groups[ $slug ] = $module->group;
			}
		}

		// Add module alias groups.
		foreach ( self::$module_aliases as $alias => $config ) {

			if ( ! $config->group || ! $config->enabled ) {
				continue;
			}

			$slug = sanitize_key( $config->group );

			if ( ! isset( $groups[ $slug ] ) ) {
				$groups[ $slug ] = $config->group;
			}
		}

		ksort( $groups );

		/**
		 * Returns an array of module group slugs and names.
		 * @see fl_builder_module_groups
		 * @since 2.2.6
		 */
		return apply_filters( 'fl_builder_module_groups', $groups );
	}

	/**
	 * Returns an array of module category slugs => names
	 *
	 * @since 2.0
	 * @return array
	 */
	static public function get_module_categories() {
		$categories = array();

		/**
		 * Use this filter to add custom module categories that will show up before the default module categories in the builder's UI.
		 * @see fl_builder_module_categories
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		foreach ( apply_filters( 'fl_builder_module_categories', array() ) as $custom_category ) {
			$categories[ $custom_category ] = array();
		}

		// Build the default category arrays.
		$categories[ __( 'Basic', 'fl-builder' ) ]    = array();
		$categories[ __( 'Box', 'fl-builder' ) ]      = array();
		$categories[ __( 'Media', 'fl-builder' ) ]    = array();
		$categories[ __( 'Actions', 'fl-builder' ) ]  = array();
		$categories[ __( 'Layout', 'fl-builder' ) ]   = array();
		$categories[ __( 'Info', 'fl-builder' ) ]     = array();
		$categories[ __( 'Posts', 'fl-builder' ) ]    = array();
		$categories[ __( 'Advanced', 'fl-builder' ) ] = array();
		$categories[ __( 'Other', 'fl-builder' ) ]    = array();

		return $categories;
	}

	/**
	 * Returns an array of categorized modules.
	 *
	 * @since 1.0
	 * @param bool $show_disabled Whether to include disabled modules in the result.
	 * @return array
	 */
	static public function get_categorized_modules( $show_disabled = false ) {
		$enabled_modules = self::get_enabled_modules();
		$widgets         = null;
		$categories      = self::get_module_categories();
		$other_key       = __( 'Other', 'fl-builder' );
		$widgets_key     = __( 'WordPress Widgets', 'fl-builder' );

		// Build the categories array.
		foreach ( self::$modules as $module ) {

			if ( ! $module->enabled ) {
				continue;
			} elseif ( ! in_array( $module->slug, $enabled_modules ) && ! $show_disabled ) {
				continue;
			} elseif ( 'widget' == $module->slug ) {
				$widgets = self::get_wp_widgets();
			} elseif ( isset( $module->category ) ) {

				if ( ! isset( $categories[ $module->category ] ) ) {
					$categories[ $module->category ] = array();
				}

				$categories[ $module->category ][ $module->name ] = $module;
			} else {
				$categories[ $other_key ][ $module->name ] = $module;
			}
		}

		// Add module aliases.
		foreach ( self::$module_aliases as $alias => $config ) {

			if ( ! $config->enabled || ! $config->slug || ! $config->category ) {
				continue;
			}
			if ( ! isset( $categories[ $config->category ] ) ) {
				$categories[ $config->category ] = array();
			}

			$categories[ $config->category ][ $config->name ] = $config;
		}

		// Add widgets if we have them.
		if ( $widgets ) {
			$categories[ $widgets_key ] = $widgets;
		}

		// Sort the modules.
		foreach ( $categories as $title => $modules ) {
			if ( count( $categories[ $title ] ) == 0 ) {
				unset( $categories[ $title ] );
			} else {
				ksort( $categories[ $title ] );
			}
		}

		// Return sorted categories.
		return $categories;
	}

	/**
	 * Similar to get_categorized_modules() but creates a flat list.
	 *
	 * @since 2.0
	 * @param bool $show_disabled Should show disabled?
	 * @return array
	 */
	static public function get_uncategorized_modules( $show_disabled = false ) {
		$enabled_modules = self::get_enabled_modules();
		$modules         = array();
		$aliases         = self::$module_aliases;
		$widgets         = FLBuilderModel::get_wp_widgets();

		foreach ( self::$modules as $module ) {

			if ( ! $module->enabled ) {
				continue;
			} elseif ( ! in_array( $module->slug, $enabled_modules ) && ! $show_disabled ) {
				continue;
			} elseif ( 'widget' === $module->slug ) {
				continue;
			}

			$module           = clone $module;
			$module->kind     = 'module';
			$module->isWidget = false; // @codingStandardsIgnoreLine
			$module->isAlias  = false; // @codingStandardsIgnoreLine
			$module->group    = $module->group ? array( sanitize_key( $module->group ) ) : array( 'standard' );

			if ( ! isset( $module->icon ) || '' == $module->icon ) {
				$module->icon = FLBuilderModule::get_default_icon();
			}

			// Remove backend-only & instance properties.
			unset( $module->css );
			unset( $module->js );
			unset( $module->editor_export );
			unset( $module->node );
			unset( $module->parent );
			unset( $module->partial_refresh );
			unset( $module->position );
			unset( $module->settings );
			unset( $module->form );
			unset( $module->dir );

			$modules[] = $module;
		}

		// Add module aliases.
		foreach ( $aliases as $alias => $config ) {

			if ( ! $config->enabled || ! $config->slug || ! $config->category || 'panel' !== $config->location ) {
				continue;
			}

			$config->kind     = 'module';
			$config->isWidget = false; // @codingStandardsIgnoreLine
			$config->isAlias = true; // @codingStandardsIgnoreLine
			$config->group    = $config->group ? array( sanitize_key( $config->group ) ) : array( 'standard' );

			$modules[] = $config;
		}

		// Add WordPress widgets.
		if ( in_array( 'widget', $enabled_modules ) ) {
			foreach ( $widgets as $widget ) {

				$data              = new stdClass();
				$widget            = (object) $widget;
				$data->id          = $widget->id;
				$data->name        = $widget->name;
				$data->class       = $widget->class;
				$data->category    = $widget->fl_category;
				$data->kind        = 'module';
				$data->accepts     = false;
				$data->isWidget = true; // @codingStandardsIgnoreLine
				$data->isAlias = false; // @codingStandardsIgnoreLine
				$data->description = isset( $widget->widget_options['description'] ) ? $widget->widget_options['description'] : '';

				$data->group = array( sanitize_key( __( 'WordPress Widgets', 'fl-builder' ) ) );

				if ( ! isset( $widget->icon ) ) {
					$data->icon = FLBuilderModule::get_widget_icon();
				}
				$modules[] = $data;
			}
		}

		return $modules;
	}

	/**
	 * Returns an instance of a module.
	 *
	 * @since 1.0
	 * @param string|object $node_id A module node ID or object.
	 * @return object|bool The module or false if it doesn't exist.
	 */
	static public function get_module( $node_id ) {
		$module = is_object( $node_id ) ? $node_id : self::get_node( $node_id );

		if ( isset( $module->settings->type ) && self::is_module_registered( $module->settings->type ) ) {

			$class              = get_class( self::$modules[ $module->settings->type ] );
			$instance           = new $class();
			$instance->node     = $module->node;
			$instance->parent   = $module->parent;
			$instance->position = $module->position;
			$instance->settings = $module->settings;
			$instance->type     = 'module';
			$instance->form     = self::$modules[ $module->settings->type ]->form;
			$instance->icon     = isset( $module->icon ) ? $module->icon : FLBuilderModule::get_default_icon();
			$instance->version  = isset( $module->version ) ? (int) $module->version : 1;

			if ( isset( $module->template_id ) ) {
				$instance->template_id      = $module->template_id;
				$instance->template_node_id = $module->template_node_id;
				$instance->template_title   = $module->template_title ?? '';
				$instance->template_url     = $module->template_url ?? '';
				$instance->global           = $module->global ?? false;
				$instance->dynamic          = $module->dynamic ?? false;
			}
			if ( isset( $module->template_root_node ) ) {
				$instance->template_root_node = true;
			}

			return $instance;
		}

		return false;
	}

	/**
	 * Returns an array of all modules in the current layout
	 * or in a column if a column id or object is supplied.
	 *
	 * @since 1.0
	 * @param string|object $col_id A column ID or object.
	 * @return array
	 */
	static public function get_modules( $col_id = null ) {
		$col       = is_object( $col_id ) ? $col_id : self::get_node( $col_id );
		$modules   = self::get_nodes( 'module', $col );
		$instances = array();
		$i         = 0;

		foreach ( $modules as $module ) {

			if ( self::is_module_registered( $module->settings->type ) ) {

				$class                     = get_class( self::$modules[ $module->settings->type ] );
				$instances[ $i ]           = new $class();
				$instances[ $i ]->node     = $module->node;
				$instances[ $i ]->parent   = $module->parent;
				$instances[ $i ]->position = $module->position;
				$instances[ $i ]->settings = $module->settings;
				$instances[ $i ]->type     = 'module';
				$instances[ $i ]->icon     = isset( $module->icon ) ? $module->icon : FLBuilderModule::get_default_icon();
				$instances[ $i ]->form     = self::$modules[ $module->settings->type ]->form;
				$instances[ $i ]->version  = isset( $module->version ) ? (int) $module->version : 1;

				if ( isset( $module->template_id ) ) {
					$instances[ $i ]->template_id      = $module->template_id;
					$instances[ $i ]->template_node_id = $module->template_node_id;
				}
				if ( isset( $module->template_root_node ) ) {
					$instances[ $i ]->template_root_node = true;
				}

				$i++;
			}
		}

		return $instances;
	}

	/**
	 * Returns an array of all modules in the current layout.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_all_modules() {
		return self::get_modules();
	}

	/**
	 * Add a new module to a column in the current layout.
	 *
	 * @since 1.0
	 * @param string $type The type of module to add.
	 * @param object $settings The new module's settings.
	 * @param string $parent_id The new module's parent node ID.
	 * @param int $position The new module's position.
	 * @return object The new module object.
	 */
	static public function add_module( $type, $settings, $parent_id = null, $position = false, $version = null ) {
		$data           = self::get_layout_data();
		$parent         = self::get_node( $parent_id );
		$module_node_id = self::generate_node_id();
		$settings->type = $type;

		if ( ! self::$modules[ $type ] ) {
			return false;
		}

		// Run module update method.
		$class              = get_class( self::$modules[ $type ] );
		$instance           = new $class();
		$instance->node     = $module_node_id;
		$instance->settings = $settings;
		$settings           = $instance->update( $settings );

		// Save the module.
		$data[ $module_node_id ]           = new StdClass();
		$data[ $module_node_id ]->node     = $module_node_id;
		$data[ $module_node_id ]->type     = 'module';
		$data[ $module_node_id ]->parent   = $parent_id;
		$data[ $module_node_id ]->position = self::next_node_position( 'module', $parent_id );
		$data[ $module_node_id ]->settings = $settings;

		// Add node template data.
		if ( self::is_node_global( $parent ) ) {
			$data[ $module_node_id ]->template_id      = $parent->template_id;
			$data[ $module_node_id ]->template_node_id = $module_node_id;
		} else {
			unset( $data[ $module_node_id ]->template_id );
			unset( $data[ $module_node_id ]->template_node_id );
			unset( $data[ $module_node_id ]->settings->dynamic_fields );
			unset( $data[ $module_node_id ]->settings->dynamic_node_settings );

		}
		unset( $data[ $module_node_id ]->template_root_node );

		// Add a version number?
		if ( null !== $version ) {
			$data[ $module_node_id ]->version = (int) $version;
		}

		// Update the layout data.
		self::update_layout_data( $data );

		// Position the module.
		if ( false !== $position ) {
			self::reorder_node( $module_node_id, $position );
		}

		// Send back the inserted module.
		return self::get_module( $module_node_id );
	}

	/**
	 * Adds a parent node for a module if a parent with the supplied
	 * parent ID doesn't exist.
	 *
	 * @since 1.6.3
	 * @param object $module The module that needs a parent node.
	 * @param string $parent_id The node ID of the parent to look for.
	 * @param int $position The position of the parent.
	 * @return string|null The new parent ID or null if none exists.
	 */
	static public function add_module_parent( $module, $parent_id = null, $position = null ) {
		$parent = ! $parent_id ? null : self::get_node( $parent_id );
		$module = self::get_module( $module );

		if ( ! is_object( $module ) ) {
			return false;
		}

		if ( ! $parent ) {
			// Add a new row if we don't have a parent. Container modules don't
			// get a parent as they can go in the main layout as of 2.9.
			if ( ! $module->accepts_children() ) {
				$row        = self::add_row( '1-col', $position );
				$col_groups = self::get_nodes( 'column-group', $row->node );
				$col_group  = array_shift( $col_groups );
				$cols       = self::get_nodes( 'column', $col_group->node );
				$parent     = array_shift( $cols );
				$parent_id  = $parent->node;
			}
		} elseif ( 'row' == $parent->type ) {
			// Add a new column group if the parent is a row.
			if ( $module->accepts_children() ) {
				$parent_id = $parent->node;
			} else {
				$col_group = self::add_col_group( $parent->node, '1-col', $position );
				$cols      = self::get_nodes( 'column', $col_group->node );
				$parent    = array_shift( $cols );
				$parent_id = $parent->node;
			}
		} elseif ( 'column-group' == $parent->type ) {
			// Add a new column if the parent is a column group.
			$parent    = self::add_col( $parent->node, $position );
			$parent_id = $parent->node;
		}

		return ! $parent_id ? null : $parent_id;
	}

	/**
	 * Returns a module's parent node of the specified type.
	 *
	 * @since 1.7
	 * @param string $type The type of parent to return.
	 * @param string|object $module_id The module's node ID. Can also be a module object.
	 * @return object The parent node.
	 */
	static public function get_module_parent( $type, $module_id ) {
		$module = is_object( $module_id ) ? $module_id : self::get_module( $module_id );
		$nodes  = self::get_categorized_nodes();

		// Check rows to find the parent for container modules.
		if ( 'row' == $type ) {
			foreach ( $nodes['rows'] as $row ) {
				if ( $row->node == $module->parent ) {
					return $row;
				}
			}
		}

		// Check column structure to find the parent.
		foreach ( $nodes['columns'] as $column ) {

			if ( $column->node == $module->parent ) {

				if ( 'column' == $type ) {
					return $column;
				}

				foreach ( $nodes['groups'] as $group ) {

					if ( $group->node == $column->parent ) {

						if ( 'column-group' == $type ) {
							return $group;
						}

						foreach ( $nodes['rows'] as $row ) {

							if ( $row->node == $group->parent ) {
								return $row;
							}
						}
					}
				}
			}
		}

		return null;
	}

	/**
	 * Add a new module with default settings to a column
	 * in the current layout.
	 *
	 * @since 1.0
	 * @param string $parent_id The new module's parent node ID.
	 * @param string $type The type of module to add.
	 * @param int $position The new module's position.
	 * @return object The new module object.
	 * @return array $defaults Default settings for the module.
	 * @return array $template Override the defined child template for this module.
	 */
	static public function add_default_module( $parent_id = null, $type = null, $position = null, $defaults = null, $template = null ) {
		$parent         = ! $parent_id ? null : self::get_node( $parent_id );
		$settings       = self::get_module_defaults( $type );
		$module_node_id = self::generate_node_id();

		// Merge default settings if present.
		if ( $defaults ) {
			$settings = (object) array_merge( (array) $settings, $defaults );
		}

		// Get an instance of the module.
		$class              = get_class( self::$modules[ $type ] );
		$instance           = new $class();
		$instance->node     = $module_node_id;
		$instance->settings = $settings;

		// Add a new parent if one is needed.
		if ( ! $parent || 'row' == $parent->type || 'column-group' == $parent->type ) {
			$parent_id = self::add_module_parent( $instance, $parent_id, $position );
			$parent    = self::get_node( $parent_id );
		}

		// Run module update method.
		$settings = $instance->update( $settings );

		// Save the module.
		$data                              = self::get_layout_data();
		$data[ $module_node_id ]           = new StdClass();
		$data[ $module_node_id ]->node     = $module_node_id;
		$data[ $module_node_id ]->type     = 'module';
		$data[ $module_node_id ]->parent   = $parent_id;
		$data[ $module_node_id ]->position = self::next_node_position( 'module', $parent_id );
		$data[ $module_node_id ]->settings = $settings;
		$data[ $module_node_id ]->version  = FLBuilderModuleDeprecations::get_module_version( $type );

		// Add node template data.
		if ( self::is_node_global( $parent ) ) {
			$data[ $module_node_id ]->template_id      = $parent->template_id;
			$data[ $module_node_id ]->template_node_id = $module_node_id;
		}

		// Update the layout data.
		self::update_layout_data( $data );

		// Position the module.
		if ( null !== $position ) {
			self::reorder_node( $module_node_id, $position );
		}

		// Insert children.
		$template = $template ? $template : $instance->template;
		if ( $instance->accepts_children() && ! empty( $template ) ) {
			self::add_default_module_template( $module_node_id, $template );
		}

		// Send back the inserted module.
		return self::get_module( $module_node_id );
	}

	/**
	 * Inserts children for a module with a template defined.
	 *
	 * @param string $module_id
	 * @param array $template
	 * @return void
	 */
	static public function add_default_module_template( $module_id, $template ) {
		$position = 0;

		foreach ( $template as $config ) {
			$module = self::add_default_module( $module_id, $config[0], $position, $config[1] );
			$position++;

			if ( isset( $config[2] ) && $module->accepts_children() ) {
				self::add_default_module_template( $module->node, $config[2] );
			}
		}
	}

	/**
	 * Make a copy of a module.
	 *
	 * @since 1.0
	 * @param string|object $node_id Node ID or module to copy.
	 * @param object $settings These settings will be used for the copy if present.
	 * @param string $parent_id Node ID of a new parent if necessary.
	 * @param int $position Position of the new module if necessary.
	 * @param bool $preserve_globals Whether to preserve global nodes.
	 * @return object|bool The new module object.
	 */
	static public function copy_module( $node_id = null, $settings = null, $parent_id = null, $position = null, $preserve_globals = true ) {

		// Duplicate as a global module?
		$node = self::get_node( $node_id );
		if ( $preserve_globals && self::is_node_template_root( $node ) ) {
			$new_node = self::duplicate_global_node( $node->node, $parent_id );
			return self::get_node( $new_node->node );
		}

		// Duplicate as a regular module...
		$module = self::get_module( $node_id );

		if ( ! $module ) {
			return false;
		} elseif ( $settings ) {
			$module->settings = (object) array_merge( (array) $module->settings, (array) $settings );
		}

		$parent_id   = null !== $parent_id ? $parent_id : $module->parent;
		$position    = null !== $position ? $position : $module->position + 1;
		$version     = isset( $module->version ) && $module->version ? (int) $module->version : null;
		$settings    = clone $module->settings;
		$module_copy = self::add_module( $settings->type, $settings, $parent_id, $position, $version );
		$children    = self::get_nodes( null, $module );

		foreach ( $children as $child ) {
			self::copy_module( $child, $child->settings, $module_copy->node, $child->position, $preserve_globals );
		}

		return $module_copy;
	}

	/**
	 * Run module specific logic on new node settings.
	 *
	 * @since 1.0
	 * @param object $module A module node object.
	 * @param object $new_settings The new settings.
	 * @return object
	 */
	static public function process_module_settings( $module, $new_settings ) {
		// Get a new node instance to work with.
		$class              = get_class( self::$modules[ $module->settings->type ] );
		$instance           = new $class();
		$instance->node     = $module->node;
		$instance->parent   = $module->parent;
		$instance->settings = $module->settings;

		// Run node delete to clear any cache.
		$instance->delete();

		// Run node update.
		$instance->settings = $new_settings;
		$new_settings       = $instance->update( $new_settings );

		return $new_settings;
	}

	/**
	 * Returns a cloned settings object for a module.
	 *
	 * @since 1.9
	 * @param object $settings
	 * @return object
	 */
	static public function clone_module_settings( $settings ) {
		$new_settings = new stdClass();

		foreach ( $settings as $key => $val ) {
			$new_settings->$key = $val;
		}

		return $new_settings;
	}

	/**
	 * Returns the default settings for a module or
	 * all modules if type is null.
	 *
	 * @since 1.0
	 * @param string|object $type The type of module or a module object.
	 * @return object|array
	 */
	static public function get_module_defaults( $type = null ) {
		$module = is_object( $type ) ? $type : null;
		$type   = $module ? $module->settings->type : $type;

		if ( $type ) {

			$defaults = new StdClass();

			if ( isset( self::$modules[ $type ]->form ) ) {
				$defaults       = self::get_settings_form_defaults( $type );
				$defaults       = self::merge_nested_module_defaults( $type, $defaults );
				$defaults->type = $type;
				$defaults       = apply_filters( 'fl_builder_module_defaults', $defaults, $module );
			}
		} else {

			$defaults = array();

			foreach ( self::$modules as $module ) {
				$defaults[ $module->slug ] = self::get_module_defaults( $module->slug );
			}
		}

		return $defaults;
	}

	/**
	 * Merges the default settings for nested forms in a module.
	 *
	 * @since 1.7
	 * @param string $type The type of module.
	 * @param object $settings The module settings object.
	 * @return object
	 */
	static public function merge_nested_module_defaults( $type, $settings ) {
		return self::merge_nested_form_defaults( 'module', $type, $settings );
	}

	/**
	 * Returns an array of data for each core WordPress widget.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_wp_widgets() {
		global $wp_widget_factory;

		$widgets = array();

		/**
		 * Array of known widgets that won't work in the builder.
		 * @see fl_get_wp_widgets_exclude
		 */
		$exclude = apply_filters( 'fl_get_wp_widgets_exclude', array(
			'WP_Widget_Media_Audio',
			'WP_Widget_Media_Image',
			'WP_Widget_Media_Video',
			'WP_Widget_Media_Gallery',
			'WP_Widget_Text',
			'WP_Widget_Custom_HTML',
			'WP_Widget_Block',
		) );

		foreach ( $wp_widget_factory->widgets as $class => $widget ) {
			if ( in_array( $class, $exclude ) ) {
				continue;
			}
			$widget->class                     = $class;
			$widget->isWidget         = true; // @codingStandardsIgnoreLine
			$widget->fl_category               = __( 'WordPress Widgets', 'fl-builder' );
			$widgets[ (string) $widget->name ] = $widget;
		}

		ksort( $widgets );

		return $widgets;
	}

	/**
	 * Returns an array of data for all registered sidebars.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_wp_sidebars() {
		global $wp_registered_sidebars;

		$sidebars = array();

		foreach ( $wp_registered_sidebars as $sidebar ) {
			$sidebars[ $sidebar['name'] ] = $sidebar;
		}

		ksort( $sidebars );

		return $sidebars;
	}

	/**
	 * Returns an array of column group data.
	 *
	 * @since 2.0
	 * @return array
	 */
	static public function get_column_groups() {
		$cols = array(
			array(
				'name'  => __( '1 Column', 'fl-builder' ),
				'id'    => '1-col',
				'count' => 1,
			),
			array(
				'name'  => __( '2 Columns', 'fl-builder' ),
				'id'    => '2-cols',
				'count' => 2,
			),
			array(
				'name'  => __( '3 Columns', 'fl-builder' ),
				'id'    => '3-cols',
				'count' => 3,
			),
			array(
				'name'  => __( '4 Columns', 'fl-builder' ),
				'id'    => '4-cols',
				'count' => 4,
			),
			array(
				'name'  => __( '5 Columns', 'fl-builder' ),
				'id'    => '5-cols',
				'count' => 5,
			),
			array(
				'name'  => __( '6 Columns', 'fl-builder' ),
				'id'    => '6-cols',
				'count' => 6,
			),
			array(
				'name'  => __( 'Left Sidebar', 'fl-builder' ),
				'id'    => 'left-sidebar',
				'count' => 2,
			),
			array(
				'name'  => __( 'Right Sidebar', 'fl-builder' ),
				'id'    => 'right-sidebar',
				'count' => 2,
			),
			array(
				'name'  => __( 'Left & Right Sidebar', 'fl-builder' ),
				'id'    => 'left-right-sidebar',
				'count' => 3,
			),
		);

		return $cols;
	}

	/**
	 * Loads the files for all core builder settings.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function load_settings() {
		require_once FL_BUILDER_DIR . 'includes/global-settings.php';
		require_once FL_BUILDER_DIR . 'includes/layout-settings.php';
		require_once FL_BUILDER_DIR . 'includes/row-settings.php';
		require_once FL_BUILDER_DIR . 'includes/column-settings.php';
		require_once FL_BUILDER_DIR . 'includes/module-settings.php';
	}

	/**
	 * Register a settings form with the builder.
	 *
	 * @since 1.0
	 * @param string $id The form id.
	 * @param array $form The form data.
	 * @return void
	 */
	static public function register_settings_form( $id, $form ) {
		/**
		 * Use this filter to modify the config array for a settings form when it is registered.
		 * @see fl_builder_register_settings_form
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		self::$settings_forms[ $id ] = apply_filters( 'fl_builder_register_settings_form', $form, $id );

		// Since 2.0 we need to store the form ID on each tab to ensure that
		// it's always available for rendering forms in JS on the frontend.
		if ( isset( self::$settings_forms[ $id ]['tabs'] ) ) {
			foreach ( self::$settings_forms[ $id ]['tabs'] as $tab_id => $tab ) {
				self::$settings_forms[ $id ]['tabs'][ $tab_id ]['form_id'] = $id;
			}
		} else {
			self::$settings_forms[ $id ]['form_id'] = $id;
		}
	}

	/**
	 * Allows settings forms to be filtered once the query
	 * is parsed (on the wp action).
	 *
	 * @since 2.10
	 */
	static public function filter_settings_forms() {
		foreach ( self::$settings_forms as $id => $form ) {
			self::$settings_forms[ $id ] = apply_filters( 'fl_builder_filter_settings_form', $form, $id );

			if ( isset( self::$modules[ $id ] ) ) {
				self::$modules[ $id ]->form = self::$settings_forms[ $id ];

				if ( isset( self::$modules[ $id ]->form['advanced'] ) ) {
					self::$modules[ $id ]->form['advanced'] = self::$settings_forms['module_advanced'];
				}

				if ( isset( self::$modules[ $id ]->form['auto_style'] ) ) {
					self::$modules[ $id ]->form['auto_style'] = self::$settings_forms['auto_style'];
				}
			} elseif ( 'module_advanced' === $id ) {
				foreach ( self::$modules as $slug => $module ) {
					self::$modules[ $slug ]->form['advanced'] = self::$settings_forms['module_advanced'];
				}
			}
		}
	}

	/**
	 * Returns the data for a settings form.
	 *
	 * @since 1.0
	 * @param string $id The form id.
	 * @return array
	 */
	static public function get_settings_form( $id ) {
		return isset( self::$settings_forms[ $id ] ) ? self::$settings_forms[ $id ] : false;
	}

	/**
	 * Returns an array of fields in a settings form.
	 *
	 * @since 1.0
	 * @param array|string $form The form data array or the form key. If key, group must be set as well.
	 * @param string The form group. Either general or module.
	 * @return array
	 */
	static public function get_settings_form_fields( $form, $group = null ) {
		$fields = array();

		if ( 'string' === gettype( $form ) ) {
			if ( 'general' === $group ) {
				$form = FLBuilderModel::$settings_forms[ $form ]['tabs'];
			} elseif ( 'module' === $group ) {
				$form = FLBuilderModel::$modules[ $form ]->form;
			} else {
				return $fields;
			}
		}

		foreach ( (array) $form as $tab ) {
			if ( isset( $tab['sections'] ) ) {
				foreach ( $tab['sections'] as $section ) {
					if ( isset( $section['fields'] ) ) {
						foreach ( $section['fields'] as $name => $field ) {
							if ( ! isset( $field['type'] ) ) {
								continue;
							}
							$fields[ $name ] = $field;
						}
					}
				}
			}
		}

		return $fields;
	}

	/**
	 * Returns a settings object with the defaults for a form.
	 *
	 * @since 1.0
	 * @param string $type The type of form.
	 * @return object
	 */
	static public function get_settings_form_defaults( $type ) {
		// Check to see if the defaults are cached first.
		if ( isset( self::$settings_form_defaults[ $type ] ) ) {
			return self::$settings_form_defaults[ $type ];
		}

		// They aren't cached, let's get them.
		$defaults = new StdClass();

		// Check the registered forms first.
		if ( isset( self::$settings_forms[ $type ] ) ) {
			$form_type = $type;
			$tabs      = self::$settings_forms[ $type ]['tabs'];
		} elseif ( isset( self::$modules[ $type ] ) ) {
			$form_type = $type . '-module';
			$tabs      = self::$modules[ $type ]->form;
		} else {
			return $defaults;
		}

		// Get the fields.
		$fields = self::get_settings_form_fields( $tabs );

		// Handle a few special cases before getting the defaults.
		foreach ( $fields as $name => $field ) {

			// Add the root name if needed later for synthetic fields like
			// the dimension fields being synthesized below.
			$fields[ $name ]['root_name'] = $name;
			$field['root_name']           = $name;

			// Handle dimension fields. We have to do it this way for backwards compat
			// with old margin, padding, and border fields as the settings expect margin_top
			// or margin_bottom to exist instead of just the margin key.
			if ( 'dimension' == $field['type'] ) {
				if ( isset( $field['keys'] ) ) {
					$keys = array_keys( $field['keys'] );
				} else {
					$keys = array( 'top', 'right', 'bottom', 'left' );
				}
				foreach ( $keys as $position ) {
					$fields[ $name . '_' . $position ] = $field;
				}
				unset( $fields[ $name ] );
			}
		}

		// Loop through the fields and get the defaults.
		foreach ( $fields as $name => $field ) {

			$default           = isset( $field['default'] ) ? $field['default'] : '';
			$is_multiple       = isset( $field['multiple'] ) && true === $field['multiple'];
			$supports_multiple = 'editor' != $field['type'] && 'service' != $field['type'];
			$responsive        = isset( $field['responsive'] ) && $field['responsive'] ? $field['responsive'] : false;

			// Get the default unit if this field has more than one unit.
			if ( isset( $field['units'] ) && is_array( $field['units'] ) && count( $field['units'] ) > 1 ) {
				$default_unit = isset( $field['default_unit'] ) ? $field['default_unit'] : $field['units'][0];
			} else {
				$default_unit = null;
			}

			// Set the default.
			if ( $is_multiple && $supports_multiple ) {
				$defaults->$name = is_array( $default ) ? $default : array( $default );
			} else {

				foreach ( array( 'default', 'large', 'medium', 'responsive' ) as $device ) {

					if ( ! $responsive && 'default' !== $device ) {
						continue;
					}

					$response_suffix = ( 'default' == $device ? '' : '_' . $device );
					$responsive_name = $name . $response_suffix;
					$unit_name       = $field['root_name'] . $response_suffix . '_unit';

					// Add the default value.
					if ( is_array( $responsive ) && isset( $responsive['default'] ) && isset( $responsive['default'][ $device ] ) ) {
						$defaults->{ $responsive_name } = $responsive['default'][ $device ];
					} elseif ( 'default' == $device ) {
						$defaults->$name = $default;
					} else {
						$defaults->{ $responsive_name } = '';
					}

					// Add the unit default value.
					if ( null !== $default_unit ) {
						if ( is_array( $responsive ) && isset( $responsive['default_unit'] ) && isset( $responsive['default_unit'][ $device ] ) ) {
							$defaults->{ $unit_name } = $responsive['default_unit'][ $device ];
						} else {
							$defaults->{ $unit_name } = $default_unit;
						}
					}

					// Add the photo source default value.
					if ( 'photo' === $field['type'] ) {
						$defaults->{ $name . $response_suffix . '_src' } = '';
					}

					// Add the link target and nofollow default values.
					if ( 'link' === $field['type'] ) {
						if ( isset( $field['show_target'] ) && $field['show_target'] ) {
							$defaults->{ $name . '_target' } = '_self';
						}
						if ( isset( $field['show_nofollow'] ) && $field['show_nofollow'] ) {
							$defaults->{ $name . '_nofollow' } = 'no';
						}
					}
				}
			}
		}

		/**
		 * Use this filter to change the defaults for any of the settings forms in the builder including global, row, column and module settings.
		 * @see fl_builder_settings_form_defaults
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		self::$settings_form_defaults[ $type ] = apply_filters( 'fl_builder_settings_form_defaults', $defaults, $form_type );

		return self::$settings_form_defaults[ $type ];
	}

	/**
	 * Merges the default settings for nested forms.
	 *
	 * @since 1.10.8
	 * @param string $type The type of form. Either general or module.
	 * @param string $form The form ID.
	 * @param object $settings The module settings object.
	 * @return object
	 */
	static public function merge_nested_form_defaults( $type, $form, $settings ) {

		// Get the fields.
		if ( 'module' === $type && isset( self::$modules[ $form ] ) ) {
			$fields = self::get_settings_form_fields( self::$modules[ $form ]->form );
		} elseif ( isset( self::$settings_forms[ $form ] ) ) {
			$fields = self::get_settings_form_fields( self::$settings_forms[ $form ]['tabs'] );
		} else {
			return $settings;
		}

		// Loop through the settings.
		foreach ( $settings as $key => $val ) {

			// Make sure this field is a nested form.
			if ( ! isset( $fields[ $key ]['form'] ) ) {
				continue;
			}

			// Get the nested form defaults.
			$nested_defaults = self::get_settings_form_defaults( $fields[ $key ]['form'] );

			// Merge the defaults.
			if ( is_array( $val ) ) {
				foreach ( $val as $nested_key => $nested_val ) {
					$settings->{ $key }[ $nested_key ] = (object) array_merge( (array) $nested_defaults, (array) $nested_val );
				}
			} elseif ( ! empty( $settings->{ $key } ) ) {
				$settings->{ $key } = (object) array_merge( (array) $nested_defaults, (array) $settings->{ $key } );
			} else {
				$settings->{ $key } = (object) $nested_defaults;
			}
		}

		return $settings;
	}

	/**
	 * Save the settings for a node.
	 *
	 * @since 1.0
	 * @param string $node_id The node ID.
	 * @param object $settings The settings to save.
	 * @return void
	 */
	static public function save_settings( $node_id = null, $settings = null ) {
		$node = self::get_node( $node_id );

		if ( empty( $node ) ) {
			return null;
		}

		$settings          = (object) $settings;
		$template_post_id  = self::is_node_global( $node );
		$is_dynamic_global = isset( $settings->dynamic_node_settings );
		$is_node_template  = self::is_post_node_template( false, $node->type );
		$post_id           = self::get_post_id();

		// Verify settings for security before proceeding.
		if ( ! FLBuilderModel::user_has_unfiltered_html() && true !== self::verify_settings( $settings ) ) {
			return array(
				'node_id'  => $node->node,
				'settings' => $node->settings,
				'layout'   => FLBuilderAJAXLayout::render(),
			);
		}

		// Merge the new settings.
		if ( $is_dynamic_global ) {
			$new_settings = FLBuilderDynamicGlobal::merge_settings_for_save( $node, $settings );
		} else {
			$new_settings = (object) array_merge( (array) $node->settings, (array) $settings );
			$new_settings = self::process_node_settings( $node, $new_settings );
		}

		// Ensure dynamic fields is an object.
		if ( isset( $new_settings->dynamic_fields ) ) {
			$new_settings->dynamic_fields = (object) $new_settings->dynamic_fields;
		}

		/**
		 * Remove any js setting for users with no unfiltered role
		 */
		if ( ! FLBuilderModel::user_has_unfiltered_html() ) {
			unset( $new_settings->bb_js_code );
		}

		// Save the settings to the node.
		$data                       = self::get_layout_data();
		$data[ $node_id ]->settings = $new_settings;

		// Update the layout data.
		self::update_layout_data( $data );

		// Save settings for a global node template?
		if ( $template_post_id && $template_post_id !== $post_id && ! $is_node_template && ! $is_dynamic_global ) {

			// Get the template data.
			$template_data = self::get_layout_data( 'published', $template_post_id );

			// Update the template node settings.
			$template_data[ $node->template_node_id ]->settings = $new_settings;

			// Save the template data.
			self::update_layout_data( $template_data, 'published', $template_post_id );
			self::update_layout_data( $template_data, 'draft', $template_post_id );

			// Delete the template asset cache.
			self::delete_all_asset_cache( $template_post_id );
			self::delete_node_template_asset_cache( $template_post_id );
		}

		// Return the processed settings and new layout.
		return array(
			'node_id'  => $node->node,
			'settings' => $new_settings,
			'layout'   => FLBuilderAJAXLayout::render(),
		);
	}

	/**
	 * Verify the settings for a node to make sure they
	 * can be saved safely.
	 *
	 * @since 2.4.1
	 * @param object $settings The settings to verify.
	 * @return bool
	 */
	static public function verify_settings( $settings ) {
		return self::verify_settings_kses( $settings );
	}

	/**
	 * Verify the settings for a node by running them through wp_kses.
	 * Any settings that have changed mean unallowed code was entered.
	 *
	 * @since 2.4.1
	 * @param object $settings The settings to verify.
	 * @return bool
	 */
	public static function verify_settings_kses( $settings ) {

		if ( ! has_filter( 'safe_style_css', '__return_empty_array' ) ) {
			add_filter( 'safe_style_css', '__return_empty_array' );
		}

		foreach ( $settings as $key => $value ) {
			if ( is_string( $value ) ) {
				$value     = stripslashes( $value );
				$sanitized = wp_kses_post( $value );
				if ( json_encode( $sanitized ) !== json_encode( self::fix_kses( $value ) ) ) {
					remove_filter( 'safe_style_css', '__return_empty_array' );
					$output = array(
						'diff'   => wp_text_diff( $value, $sanitized, array( 'show_split_view' => false ) ),
						'value'  => self::fix_kses( $value ),
						'parsed' => $sanitized,
						'key'    => $key,
					);
					return $output;
				}
			} else {
				if ( is_object( $value ) || is_array( $value ) ) {
					$check = self::verify_settings_kses( $value );
					if ( isset( $check['diff'] ) ) {
						remove_filter( 'safe_style_css', '__return_empty_array' );
						return $check;
					}
				}
			}
		}

		remove_filter( 'safe_style_css', '__return_empty_array' );
		return true;
	}

	/**
	 * Add a space to self closing tags and other things if there isn't one because kses will and checks will fail.
	 * @since 2.4.2
	 */
	static public function fix_kses( $value ) {

		// fix & -> &amp;
		$value = preg_replace( '/&([a-z0-9#]+);/i', '&$1;', $value );
		$value = preg_replace( '#(&)(?!(.*);)#i', '&amp;', $value );

		// fix <br/> -> <br />
		$value = preg_replace( '#(<[a-z]+)(\/>)#', '$1 $2', $value );

		return $value;
	}


	/**
	 * Sanitizes settings for a form.
	 *
	 * @since 2.0
	 * @param string $form
	 * @param string $group
	 * @param object $settings
	 * @return object
	 */
	static public function sanitize_settings( $settings, $form, $group ) {
		$fields = FLBuilderModel::get_settings_form_fields( $form, $group );

		foreach ( $settings as $name => $value ) {
			if ( ! isset( $fields[ $name ] ) ) {
				continue;
			}
			if ( isset( $fields[ $name ]['sanitize'] ) ) {
				if ( is_array( $fields[ $name ]['sanitize'] ) ) {
					$settings->$name = call_user_func_array( $fields[ $name ]['sanitize'][0], array( $value, $fields[ $name ]['sanitize'][1] ) );
				} else {
					$settings->$name = call_user_func_array( $fields[ $name ]['sanitize'], array( $value ) );
				}
			} elseif ( 'link' === $fields[ $name ]['type'] ) {
				$settings->$name = FLBuilderUtils::esc_attr( $value );
			} elseif ( 'container_element' === $name ) {
				$clean   = false;
				$allowed = apply_filters( 'fl_builder_node_container_element_options', array(
					'div'     => '&lt;div&gt;',
					'section' => '&lt;section&gt;',
					'article' => '&lt;article&gt;',
					'aside'   => '&lt;aside&gt;',
					'main'    => '&lt;main&gt;',
					'header'  => '&lt;header&gt;',
					'footer'  => '&lt;footer&gt;',
				) );
				foreach ( $allowed as $el => $txt ) {
					if ( $value === $el ) {
						$clean = true;
					}
				}
				if ( ! $clean ) {
					$settings->$name = 'div';
				}
			}
		}

		return $settings;
	}

	/**
	 * Adds slashes to settings going into the database as WordPress
	 * removes them when we save using update_metadata. This is done
	 * to ensure slashes in user input aren't removed.
	 *
	 * @since 1.5.6
	 * @param mixed $data The data to slash.
	 * @return mixed The slashed data.
	 */
	static public function slash_settings( $data ) {
		if ( is_array( $data ) ) {
			foreach ( $data as $key => $val ) {
				$data[ $key ] = self::slash_settings( $val );
			}
		} elseif ( is_object( $data ) ) {
			foreach ( $data as $key => $val ) {
				$data->$key = self::slash_settings( $val );
			}
		} elseif ( is_string( $data ) ) {
			$data = wp_slash( $data );
		}

		return $data;
	}

	/**
	 * Merge defaults into a settings object.
	 *
	 * @since 1.0
	 * @param object $settings Reference to a settings object.
	 * @param array $defaults The defaults to merge in.
	 * @return void
	 */
	static public function default_settings( &$settings, $defaults ) {
		foreach ( $defaults as $name => $value ) {
			if ( ! isset( $settings->$name ) ) {
				$settings->$name = $value;
			}
		}
	}

	/**
	 * Get the global builder settings.
	 *
	 * @since 1.0
	 * @param bool $cached - Since 2.9
	 * @return object
	 */
	static public function get_global_settings( $cached = true ) {
		if ( ! $cached || null === self::$global_settings ) {
			$settings = get_option( '_fl_builder_settings' );
			$defaults = self::get_settings_form_defaults( 'global' );

			if ( ! $settings ) {
				$settings = new StdClass();
			}

			// Special handling for color scheme
			if ( is_array( $settings ) ) {
				$settings['color_scheme'] = FLBuilderUserSettings::get_color_scheme();
			} else {
				$settings->color_scheme = FLBuilderUserSettings::get_color_scheme();
			}

			// Merge in defaults and cache settings
			self::$global_settings = (object) array_merge( (array) $defaults, (array) $settings );
			self::$global_settings = self::merge_nested_form_defaults( 'general', 'global', self::$global_settings );
			self::$global_settings = apply_filters( 'fl_builder_get_global_settings', self::$global_settings );
		}

		return self::$global_settings;
	}

	/**
	 * Save the global builder settings.
	 *
	 * @since 1.0
	 * @param array $settings The new global settings.
	 * @return object
	 */
	static public function save_global_settings( $settings = array() ) {

		if ( ! current_user_can( 'delete_others_posts' ) ) {
			wp_die( '', 403 );
		}

		$old_settings = self::get_global_settings();
		$settings     = self::sanitize_global( $settings );

		if ( ! current_user_can( 'unfiltered_html' ) ) {
			if ( is_object( $settings ) ) {
				unset( $settings->js );
			} else {
				unset( $settings['js'] );
			}
		}

		$new_settings = (object) array_merge( (array) $old_settings, (array) $settings );

		/**
		* Special handling for color scheme
		* Color scheme is actually a user setting, not a global setting. It just appears in the global settings form.
		*/
		if ( property_exists( $new_settings, 'color_scheme' ) ) {

			FLBuilderUserSettings::save_color_scheme( $new_settings->color_scheme );

			// No need to store this in global settings
			unset( $new_settings->color_scheme );
		}

		// delete all posts assets.
		self::delete_asset_cache_for_all_posts();

		// remove old global settings.
		self::$global_settings = null;

		// apply filter before update.
		$new_settings = apply_filters( 'fl_builder_before_save_global_settings', $new_settings );

		// update db with new settings.
		FLBuilderUtils::update_option( '_fl_builder_settings', $new_settings, true );

		return self::get_global_settings();
	}

	/**
	 * Sanitize global options on save.
	 * @since 2.2.2
	 */
	static public function sanitize_global( $settings ) {

		$fields = self::get_settings_form_fields( 'global', 'general' );

		foreach ( $settings as $name => $value ) {
			if ( ! isset( $fields[ $name ] ) ) {
				continue;
			} elseif ( isset( $fields[ $name ]['sanitize'] ) ) {
				$settings[ $name ] = call_user_func_array( $fields[ $name ]['sanitize'], array( $value ) );
			}
		}
		return $settings;
	}

	/**
	 * Duplicate the current post.
	 *
	 * @since 1.0
	 * @return int The new post ID.
	 */
	static public function duplicate_post( $post_id = false ) {
		global $wpdb;

		$post_id      = ( ! $post_id ) ? self::get_post_id() : $post_id;
		$post         = get_post( $post_id );
		$current_user = wp_get_current_user();
		$template_id  = false;

		// Duplicate the post.
		$data = array(
			'comment_status' => $post->comment_status,
			'ping_status'    => $post->ping_status,
			'post_author'    => $current_user->ID,
			'post_content'   => $post->post_content,
			'post_excerpt'   => $post->post_excerpt,
			'post_name'      => $post->post_name . '-copy',
			'post_parent'    => $post->post_parent,
			'post_password'  => $post->post_password,
			'post_status'    => 'draft',
			/* translators: %s: post/page title */
			'post_title'     => sprintf( _x( 'Copy of %s', '%s stands for post/page title.', 'fl-builder' ), $post->post_title ),
			'post_type'      => $post->post_type,
			'to_ping'        => $post->to_ping,
			'menu_order'     => $post->menu_order,
		);

		// Get the new post id.
		$new_post_id = wp_insert_post( $data );

		// Duplicate post meta.
		$post_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d", $post_id ) );

		if ( count( $post_meta ) !== 0 ) {

			foreach ( $post_meta as $meta_info ) {
				$meta_key = $meta_info->meta_key;

				if ( '_fl_builder_template_id' == $meta_key ) {
					$meta_value = self::generate_node_id();
				} else {
					$meta_value = $meta_info->meta_value;
				}

				$wpdb->insert(
					$wpdb->postmeta,
					array(
						'post_id'    => $new_post_id,
						'meta_key'   => $meta_key,
						'meta_value' => $meta_value,
					),
					array( '%d', '%s', '%s' )
				);
			}
		}

		// Flush the cache so new meta is returned in wp meta functions.
		wp_cache_flush();

		// Duplicate post terms.
		$taxonomies = get_object_taxonomies( $post->post_type );

		foreach ( $taxonomies as $taxonomy ) {

			$post_terms = wp_get_object_terms( $post_id, $taxonomy );

			for ( $i = 0; $i < count( $post_terms ); $i++ ) {
				wp_set_object_terms( $new_post_id, $post_terms[ $i ]->slug, $taxonomy, true );
			}
		}

		// Get the duplicated layout data.
		$data = self::get_layout_data( 'published', $new_post_id );

		// Generate new node ids.
		$data = self::generate_new_node_ids( $data );

		// Update template ID and template node ID
		$template_id = get_post_meta( $new_post_id, '_fl_builder_template_id', true );
		$global      = get_post_meta( $post_id, '_fl_builder_template_global', true );

		if ( $template_id && $global ) {
			foreach ( $data as $node_id => $node ) {
				$data[ $node_id ]->template_id      = $template_id;
				$data[ $node_id ]->template_node_id = $node_id;
			}
		}

		// Save the duplicated layout data.
		self::update_layout_data( $data, 'published', $new_post_id );

		// Also update draft data
		self::update_layout_data( $data, 'draft', $new_post_id );

		// Return the new post id.
		return $new_post_id;
	}

	/**
	 * Deletes all layout data and asset cache for a post.
	 *
	 * @since 1.0
	 * @param int $post_id The post ID to delete data and cache for.
	 * @return void
	 */
	static public function delete_post( $post_id ) {
		// If this is a global template, unlink it from other posts.
		self::unlink_global_node_template_from_all_posts( $post_id );

		// Delete all published and draft data.
		self::delete_layout_data( 'published', $post_id );
		self::delete_layout_data( 'draft', $post_id );

		// Delete all css and js.
		self::delete_all_asset_cache( $post_id );
	}

	/**
	 * Save a revision of a builder layout.
	 *
	 * @since 1.0
	 * @param int $post_id
	 * @return void
	 */
	static public function save_revision( $post_id, $post, $update ) {

		$parent_id = wp_is_post_revision( $post_id );

		if ( $parent_id ) {

			$parent   = get_post( $parent_id );
			$data     = self::get_layout_data( 'published', $parent->ID );
			$settings = self::get_layout_settings( 'published', $parent->ID );

			if ( ! empty( $data ) ) {
				self::update_layout_data( $data, 'published', $post_id );
				self::update_layout_settings( $settings, 'published', $post_id );
			}
		}
	}

	/**
	 * Limit the amount of revisions possible for fl-builder-template type.
	 * @since 2.1.5
	 */
	static public function limit_revisions( $num, $post ) {

		if ( 'fl-builder-template' == $post->post_type ) {
			/**
			 * Limit the amount of revisions for the fl-builder-template type.
			 * @see fl_builder_template_revisions
			 */
			$num = apply_filters( 'fl_builder_template_revisions', 25 );
		}
		return $num;
	}

	/**
	 * Maybe save a post revision when templates/rows etc are published.
	 * @since 2.1.5
	 */
	static public function save_layout_revision( $post_id ) {
		add_filter( 'wp_save_post_revision_post_has_changed', array( __CLASS__, 'save_layout_revision_changed_filter' ), 10, 3 );
		wp_save_post_revision( $post_id );
		remove_filter( 'wp_save_post_revision_post_has_changed', array( __CLASS__, 'save_layout_revision_changed_filter' ), 10, 3 );
	}

	/**
	 * Filter save_layout_revision_changed_filter return true here if the builder data has $post_has_changed
	 * this forces a post revision.
	 * @since 2.1.5
	 */
	static public function save_layout_revision_changed_filter( $post_has_changed, $last_revision, $post ) {

		// get builder data for compare
		$old = serialize( get_post_meta( $last_revision->ID, '_fl_builder_data', true ) );
		$new = serialize( get_post_meta( $post->ID, '_fl_builder_data', true ) );

		return $old != $new;
	}

	/**
	 * Restore a revision of a builder layout.
	 *
	 * @since 1.0
	 * @param int $post_id
	 * @param int $revision_id
	 * @return void
	 */
	static public function restore_revision( $post_id, $revision_id ) {
		$post     = get_post( $post_id );
		$revision = get_post( $revision_id );

		if ( $revision ) {

			$data     = self::get_layout_data( 'published', $revision->ID );
			$settings = self::get_layout_settings( 'published', $revision->ID );

			if ( ! empty( $data ) ) {
				self::update_layout_data( $data, 'published', $post_id );
				self::update_layout_data( $data, 'draft', $post_id );
				self::update_layout_settings( $settings, 'published', $post_id );
				self::update_layout_settings( $settings, 'draft', $post_id );
			} else {
				self::delete_layout_data( 'published', $post_id );
				self::delete_layout_data( 'draft', $post_id );
				self::delete_layout_settings( 'published', $post_id );
				self::delete_layout_settings( 'draft', $post_id );
			}

			self::delete_all_asset_cache( $post_id );
		}
	}

	/**
	 * Get all of the layout data for a post. We use get_metadata
	 * here instead of get_post_meta to ensure revisions are queried accordingly.
	 *
	 * @since 1.0
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to get data for.
	 * @return array
	 */
	static public function get_layout_data( $status = null, $post_id = null ) {
		$post_id = ! $post_id ? self::get_post_id() : $post_id;
		$status  = ! $status ? self::get_node_status() : $status;

		// Get layout metadata.
		if ( 'published' == $status || 'revision' == get_post_type( $post_id ) ) {
			if ( isset( self::$published_layout_data[ $post_id ] ) ) {
				$data = self::$published_layout_data[ $post_id ];
			} else {
				$data                                    = get_metadata( 'post', $post_id, '_fl_builder_data', true );
				$data                                    = self::clean_layout_data( $data );
				$data                                    = FLBuilderSettingsCompat::filter_layout_data( $data );
				self::$published_layout_data[ $post_id ] = apply_filters( 'fl_builder_get_layout_metadata', $data, $status, $post_id );
			}
		} elseif ( 'draft' == $status ) {
			if ( isset( self::$draft_layout_data[ $post_id ] ) ) {
				$data = self::$draft_layout_data[ $post_id ];
			} else {
				$data                                = get_metadata( 'post', $post_id, '_fl_builder_draft', true );
				$data                                = self::clean_layout_data( $data );
				$data                                = FLBuilderSettingsCompat::filter_layout_data( $data );
				self::$draft_layout_data[ $post_id ] = apply_filters( 'fl_builder_get_layout_metadata', $data, $status, $post_id );
			}
		}

		// Make sure we have an array.
		if ( empty( $data ) ) {
			$data = array();
		}

		// Clone the layout data to ensure the cache remains intact.
		foreach ( $data as $node_id => $node ) {
			if ( is_object( $node ) ) {
				$data[ $node_id ] = clone $node;

				if ( ! empty( $data[ $node_id ]->global ) ) {
					if ( isset( $data[ $node_id ]->type ) && 'module' === $data[ $node_id ]->type ) {
						$data[ $node_id ]->moduleType = $data[ $node_id ]->settings->type;
					}
				}
			}
		}

		/**
		 * Return the data.
		 * @see fl_builder_layout_data
		 */
		return apply_filters( 'fl_builder_layout_data', $data, $status, $post_id );
	}

	/**
	 * Update the layout data for a post. We use update_metadata
	 * here instead of update_post_meta to ensure revisions are updated accordingly.
	 *
	 * @since 1.0
	 * @param array $data The layout data to update.
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to update.
	 * @return void
	 */
	static public function update_layout_data( $data, $status = null, $post_id = null ) {
		$post_id  = ! $post_id ? self::get_post_id() : $post_id;
		$status   = ! $status ? self::get_node_status() : $status;
		$key      = 'published' == $status ? '_fl_builder_data' : '_fl_builder_draft';
		$raw_data = get_metadata( 'post', $post_id, $key );

		// Clean the layout data.
		$data = self::slash_settings( self::clean_layout_data( $data ) );

		/**
		 * @since 2.6
		 * @see fl_builder_enable_small_data_mode
		 */
		if ( apply_filters( 'fl_builder_enable_small_data_mode', false ) ) {
			if ( 'published' === $status ) {
				foreach ( $data as $node_id => $node ) {
					if ( isset( $node->settings ) ) {
						$data[ $node_id ]->settings = (object) self::array_remove_by_values( (array) $node->settings, array( '', null, array() ) );
					}
				}
			}
		}

		// Allow data to be filtered before updating.
		$data = apply_filters( 'fl_builder_before_update_layout_data', $data, $status, $post_id );

		// Update the data.
		if ( 0 === count( $raw_data ) ) {
			add_metadata( 'post', $post_id, $key, $data );
		} else {
			update_metadata( 'post', $post_id, $key, $data );
		}

		// Cache the data.
		if ( 'published' == $status ) {
			self::$published_layout_data[ $post_id ] = $data;
		} elseif ( 'draft' == $status ) {
			self::$draft_layout_data[ $post_id ] = $data;
		}
	}

	/**
	 * Delete the layout data for a post.
	 *
	 * @since 1.0
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to delete data.
	 * @return void
	 */
	static public function delete_layout_data( $status = null, $post_id = null ) {
		// Make sure we have a status to delete.
		if ( ! $status ) {
			return;
		}

		// Get the post id.
		$post_id = ! $post_id ? self::get_post_id() : $post_id;

		// Get the data to delete.
		$data = self::get_layout_data( $status, $post_id );

		// Delete the nodes.
		foreach ( $data as $node ) {
			self::call_module_delete( $node );
		}

		// Update the layout data.
		self::update_layout_data( array(), $status, $post_id );
	}

	/**
	 * Ensures the integrity of layout data key/value pairs.
	 *
	 * Also makes sure we're not serializing any FLBuilderModule
	 * instances because those are too big and bloat the data array.
	 *
	 * @since 1.0
	 * @param array $data An array of layout data.
	 * @return array
	 */
	static public function clean_layout_data( $data = array() ) {
		$cleaned = array();

		if ( is_array( $data ) ) {

			foreach ( $data as $node ) {

				if ( is_object( $node ) && isset( $node->node ) ) {

					if ( is_a( $node, 'FLBuilderModule' ) ) {
						$cleaned[ $node->node ]           = new StdClass();
						$cleaned[ $node->node ]->node     = $node->node;
						$cleaned[ $node->node ]->type     = $node->type;
						$cleaned[ $node->node ]->parent   = $node->parent;
						$cleaned[ $node->node ]->position = $node->position;
						$cleaned[ $node->node ]->settings = $node->settings;

						if ( isset( $node->version ) ) {
							$cleaned[ $node->node ]->version = $node->version;
						}
					} else {
						$cleaned[ $node->node ] = $node;
					}

					$cleaned[ $node->node ] = self::clean_node_settings( $cleaned[ $node->node ] );

					// Remove erroneous dynamic_node_settings from non-root nodes.
					// This property belongs only on template root nodes and was incorrectly
					// added to child modules by a previous outline panel label save bug.
					if ( isset( $cleaned[ $node->node ]->settings->dynamic_node_settings ) && empty( $node->template_root_node ) ) {
						unset( $cleaned[ $node->node ]->settings->dynamic_node_settings );
					}

					$cleaned[ $node->node ]->global  = self::is_node_global( $node );
					$cleaned[ $node->node ]->dynamic = self::is_node_dynamic( $node );

				}
			}
		}
		return $cleaned;
	}

	/**
	 * Low level method to clean node settings before saving or
	 * after pulling from the database. Use with caution.
	 *
	 * @since 2.10
	 * @param object $node
	 * @return object
	 */
	static public function clean_node_settings( $node ) {
		if ( ! isset( $node->settings ) || ! is_object( $node->settings ) ) {
			return $node;
		}

		foreach ( $node->settings as $key => $value ) {
			if ( str_starts_with( $key, 'flrich' ) ) {
				unset( $node->settings->{ $key } );
			}
		}

		return $node;
	}

	/**
	* Remove all empty values from the settings object recursively to save ~60% db size
	* @since 2.3
	* @param array $haystack
	* @param array $values
	* @param array $whitelist
	*
	* @return array
	*/
	static public function array_remove_by_values( $haystack, $values, $whitelist = array( 'animation', 'style', 'post_columns', 0, 1, 3, 4, 5 ) ) {
		foreach ( $haystack as $key => $value ) {
			if ( is_array( $value ) ) {
				$haystack[ $key ] = self::array_remove_by_values( $haystack[ $key ], $values );
			}

			if ( in_array( $haystack[ $key ], $values, true ) && ! in_array( $key, $whitelist ) ) {
				unset( $haystack[ $key ] );
			}
		}

		return $haystack;
	}


	/**
	 * Detect if the current layout has previously drafted changes.
	 *
	 * @since 2.0
	 * @return bool
	 */
	static public function layout_has_drafted_changes() {
		$post_id   = FLBuilderModel::get_post_id();
		$published = serialize( self::get_layout_data( 'published', $post_id ) );
		$draft     = serialize( self::get_layout_data( 'draft', $post_id ) );

		if ( $published != $draft ) {
			return true;
		}

		return false;
	}

	/**
	 * Get the builder settings for a layout.
	 *
	 * @since 1.7
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to get settings for.
	 * @return object
	 */
	static public function get_layout_settings( $status = null, $post_id = null ) {
		$status   = ! $status ? self::get_node_status() : $status;
		$post_id  = ! $post_id ? self::get_post_id() : $post_id;
		$key      = 'published' == $status ? '_fl_builder_data_settings' : '_fl_builder_draft_settings';
		$settings = get_metadata( 'post', $post_id, $key, true );
		$defaults = self::get_settings_form_defaults( 'layout' );

		if ( ! $settings ) {
			$settings = new StdClass();
		}

		$settings = (object) array_merge( (array) $defaults, (array) $settings );

		return apply_filters( 'fl_builder_layout_settings', $settings, $status, $post_id );
	}

	/**
	 * Updates the layout settings for a post.
	 *
	 * @since 1.7
	 * @param array $settings The new layout settings.
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to update.
	 * @return object
	 */
	static public function update_layout_settings( $settings = array(), $status = null, $post_id = null ) {
		$settings = (array) $settings;

		if ( ! FLBuilderUserAccess::current_user_can( 'unrestricted_editing' ) ) {
			if ( is_object( $settings ) ) {
				unset( $settings->js );
			} else {
				unset( $settings['js'] );
			}
		}
		$status       = ! $status ? self::get_node_status() : $status;
		$post_id      = ! $post_id ? self::get_post_id() : $post_id;
		$key          = 'published' == $status ? '_fl_builder_data_settings' : '_fl_builder_draft_settings';
		$raw_settings = get_metadata( 'post', $post_id, $key );
		$old_settings = self::get_layout_settings( $status, $post_id );
		$new_settings = (object) array_merge( (array) $old_settings, (array) $settings );

		if ( 0 === count( $raw_settings ) ) {
			add_metadata( 'post', $post_id, $key, self::slash_settings( $new_settings ) );
		} else {
			update_metadata( 'post', $post_id, $key, self::slash_settings( $new_settings ) );
		}

		return $new_settings;
	}

	/**
	 * Called via AJAX to save the layout settings.
	 *
	 * @since 1.7
	 * @param array $settings The new layout settings.
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of the post to update.
	 * @return object
	 */
	static public function save_layout_settings( $settings = array(), $status = null, $post_id = null ) {
		return self::update_layout_settings( $settings, $status, $post_id );
	}

	/**
	 * Delete the layout settings for a post.
	 *
	 * @since 1.7
	 * @param string $status Either published or draft.
	 * @param int $post_id The ID of a post whose settings to delete.
	 * @return void
	 */
	static public function delete_layout_settings( $status = null, $post_id = null ) {
		$status  = ! $status ? self::get_node_status() : $status;
		$post_id = ! $post_id ? self::get_post_id() : $post_id;
		$key     = 'published' == $status ? '_fl_builder_data_settings' : '_fl_builder_draft_settings';

		update_metadata( 'post', $post_id, $key, array() );
	}

	/**
	 * Merge two sets of layout settings together.
	 *
	 * @since 1.7
	 * @param object $settings The layout settings to merge into.
	 * @param object $merge_settings The layout settings to merge.
	 * @return object
	 */
	static public function merge_layout_settings( $settings, $merge_settings ) {
		$keys = array( 'css', 'js' );

		foreach ( $keys as $key ) {

			if ( empty( $merge_settings->{$key} ) ) {
				continue;
			} elseif ( strstr( $settings->{$key}, $merge_settings->{$key} ) ) {
				continue;
			} else {

				if ( ! empty( $settings->{$key} ) ) {
					$settings->{$key} .= "\n";
				}

				$settings->{$key} .= $merge_settings->{$key};
			}
		}

		return $settings;
	}

	/**
	 * Clears a draft layout and saves a new draft using
	 * the currently published layout data.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function clear_draft_layout() {
		$post_id  = self::get_post_id();
		$data     = self::get_layout_data( 'published', $post_id );
		$settings = self::get_layout_settings( 'published', $post_id );

		// Delete the old draft layout.
		self::delete_layout_data( 'draft' );

		// Save the new draft layout.
		self::update_layout_data( $data, 'draft', $post_id );

		// Save the new draft layout settings.
		self::update_layout_settings( $settings, 'draft', $post_id );

		// Clear the asset cache.
		self::delete_all_asset_cache( $post_id );
	}

	/**
	 * Saves layout data when a user chooses to publish.
	 *
	 * @since 1.0
	 * @param bool $publish Whether to publish the parent post or not.
	 * @return void
	 */
	static public function save_layout( $publish = true ) {
		$editor_content = FLBuilder::render_editor_content();
		$post_id        = self::get_post_id();
		$data           = self::get_layout_data( 'draft', $post_id );
		$settings       = self::get_layout_settings( 'draft', $post_id );

		/**
		 * This action allows you to hook into before the data is saved for a layout.
		 * @see fl_builder_before_save_layout
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		do_action( 'fl_builder_before_save_layout', $post_id, $publish, $data, $settings );

		// Delete the old published layout.
		self::delete_layout_data( 'published', $post_id );
		self::delete_layout_settings( 'published', $post_id );

		// Save the new published layout.
		self::update_layout_data( $data, 'published', $post_id );
		self::update_layout_settings( $settings, 'published', $post_id );

		// Clear the asset cache.
		self::delete_all_asset_cache( $post_id );
		self::delete_node_template_asset_cache( $post_id );

		// Enable the builder to take over the post content.
		self::enable();

		// Get the post status.
		$post_status = get_post_status( $post_id );

		// Publish the post?
		if ( $publish ) {

			$is_draft   = strstr( $post_status, 'draft' );
			$is_pending = strstr( $post_status, 'pending' );

			if ( current_user_can( 'publish_posts' ) ) {
				$post_status = $is_draft || $is_pending ? 'publish' : $post_status;
			} elseif ( $is_draft ) {
				$post_status = 'pending';
			}
		}

		// Update the post with stripped down content.
		wp_update_post(array(
			'ID'           => self::get_post_id(),
			'post_status'  => $post_status,
			'post_content' => $editor_content,
		));

		// Reset active state so assets render with published suffix.
		self::reset_active();

		// Rerender the assets for this layout.
		FLBuilder::render_assets();

		self::cleanup_post_data( $post_id );

		/**
		 * This action allows you to hook into after the data is saved for a layout.
		 * @see fl_builder_after_save_layout
		 * @link https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/common-beaver-builder-filter-examples
		 */
		do_action( 'fl_builder_after_save_layout', $post_id, $publish, $data, $settings );
	}

	/**
	 * Publishes the current builder layout only if the parent post
	 * is still a draft. The layout will be published but the parent
	 * post will remain a draft so the post can be scheduled and the
	 * layout can be viewed while the builder is not active. If the
	 * parent post is already published, nothing happens.
	 *
	 * @since 1.6.1
	 * @return void
	 */
	static public function save_draft() {
		$post_id     = self::get_post_id();
		$post_status = get_post_status( $post_id );

		if ( strstr( $post_status, 'draft' ) ) {
			self::save_layout( false );
		}

		self::cleanup_post_data( $post_id, false );

		/**
		 * After draft is saved.
		 * @see fl_builder_after_save_draft
		 */
		do_action( 'fl_builder_after_save_draft', $post_id, $post_status );
	}

	static public function cleanup_post_data( $post_id, $history = true ) {

		// remove any post lock
		delete_post_meta( $post_id, '_edit_lock' );

		if ( ! $history ) {
			return;
		}

		// delete old states
		FLBuilderHistoryManager::delete_states( $post_id );
	}


	/**
	 * Duplicates a layout for WPML when the copy from original
	 * button has been clicked.
	 *
	 * @since 1.1.7
	 * @param int $original_post_id
	 * @param int $new_post_id
	 * @return array
	 */
	static public function duplicate_wpml_layout( $original_post_id = null, $new_post_id = null ) {
		$post_data        = self::get_post_data();
		$original_post_id = isset( $post_data['original_post_id'] ) ? $post_data['original_post_id'] : $original_post_id;
		$new_post_id      = isset( $post_data['post_id'] ) ? $post_data['post_id'] : $new_post_id;
		$enabled          = get_post_meta( $original_post_id, '_fl_builder_enabled', true );
		$published        = self::get_layout_data( 'published', $original_post_id );
		$draft            = self::get_layout_data( 'draft', $original_post_id );

		$orig_post = get_post( $original_post_id );
		$new_post  = get_post( $new_post_id );

		if ( $orig_post->post_author !== $new_post->post_author ) {
			wp_die( false, false, array( 'response' => 403 ) );
		}

		check_admin_referer( 'fl_wpml_duplicate_clicked' );

		$response = array(
			'enabled'    => false,
			'has_layout' => false,
		);

		if ( ! empty( $enabled ) ) {
			update_post_meta( $new_post_id, '_fl_builder_enabled', true );
			$response['enabled'] = true;
		}
		if ( ! empty( $published ) ) {
			self::update_layout_data( $published, 'published', $new_post_id );
			$response['has_layout'] = true;
		}
		if ( ! empty( $draft ) ) {
			self::update_layout_data( $draft, 'draft', $new_post_id );
			$response['has_layout'] = true;
		}

		return $response;
	}

	/**
	 * Returns the type of templates that are enabled.
	 *
	 * @since 1.1.3
	 * @return string
	 */
	static public function get_enabled_templates() {
		$value = self::get_admin_settings_option( '_fl_builder_enabled_templates', true );

		return ! $value ? 'enabled' : $value;
	}

	/**
	 * Checks to see if the current post is a user template.
	 *
	 * @since 1.6.3
	 * @param string $type The type of user template to check for.
	 * @return bool
	 */
	static public function is_post_user_template( $type = null ) {
		$post = FLBuilderModel::get_post();

		if ( ! $post ) {
			return false;
		} elseif ( 'fl-builder-template' == $post->post_type ) {

			if ( null === $type ) {
				return true;
			} else {

				$saved_type = self::get_user_template_type( $post->ID );

				if ( $saved_type == $type ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks to see if the current post is a container module template.
	 *
	 * @since 2.8
	 * @return bool
	 */
	static public function is_post_container_module_template() {

		if ( ! self::is_post_user_template( 'module' ) ) {
			return false;
		}

		$root   = self::get_node_template_root( 'module' );
		$module = self::get_module( $root );

		if ( ! $module->accepts_children() ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks to see if the current post is a module templates with
	 * a single module that doesn't accept children.
	 *
	 * @since 2.8
	 * @return bool
	 */
	static public function is_post_leaf_module_template() {

		if ( ! self::is_post_user_template( 'module' ) ) {
			return false;
		}

		$root   = self::get_node_template_root( 'module' );
		$module = self::get_module( $root );

		if ( ! $module ) {
			return false;
		}

		if ( $module->accepts_children() ) {
			return false;
		}

		return true;
	}

	/**
	 * Saves a user defined template via AJAX.
	 *
	 * @since 1.1.3
	 * @return void
	 */
	static public function save_user_template( $settings = array() ) {
		// Save the user template post.
		$post_id = wp_insert_post(array(
			'post_title'     => $settings['name'],
			'post_type'      => 'fl-builder-template',
			'post_status'    => 'publish',
			'ping_status'    => 'closed',
			'comment_status' => 'closed',
		));

		// Set the template type.
		wp_set_post_terms( $post_id, 'layout', 'fl-builder-template-type' );

		// Add category
		$cat       = isset( $settings['category'] ) ? $settings['category'] : '';
		$cat_added = '';
		if ( __( 'Uncategorized', 'fl-builder' ) !== $cat && 'uncategorized' !== $cat ) {
			$cat_added = wp_set_object_terms( $post_id, $cat, 'fl-builder-template-category' );
		}

		// Get the layout data and settings to copy.
		$data            = self::get_layout_data();
		$layout_settings = self::get_layout_settings();

		// Generate new node ids.
		$data = self::generate_new_node_ids( $data );

		// Save the template layout data and settings.
		self::update_layout_data( $data, 'published', $post_id );
		self::update_layout_settings( $layout_settings, 'published', $post_id );

		// Enable the builder for this template.
		update_post_meta( $post_id, '_fl_builder_enabled', true );

		/**
		 * Allow extensions to hook into saving a user template.
		 * @see fl_builder_after_save_user_template
		 */
		do_action( 'fl_builder_after_save_user_template', $post_id );

		$response = array(
			'name'     => $settings['name'],
			'id'       => get_post_meta( $post_id, '_fl_builder_template_id', true ),
			'postId'   => $post_id,
			'image'    => FLBuilder::plugin_url() . 'img/templates/blank.jpg',
			'kind'     => 'template',
			'content'  => 'layout',
			'type'     => 'user',
			'isGlobal' => false,
			'link'     => add_query_arg( 'fl_builder', '', get_permalink( $post_id ) ),
			'category' => array(),
		);

		if ( is_array( $cat_added ) && ! empty( $cat_added ) ) {
			$term                                = get_term( $cat_added[0] );
			$response['category'][ $term->slug ] = $term->name;
		} else {
			$response['category']['uncategorized'] = __( 'Uncategorized', 'fl-builder' );
		}

		return $response;
	}

	/**
	 * Returns data for all user defined templates.
	 *
	 * @since 1.1.3
	 * @since 1.5.7 Added support for template categories.
	 * @param string $type The type of user template to return.
	 * @return array
	 */
	static public function get_user_templates( $type = 'layout' ) {

		if ( isset( self::$get_user_templates_cache[ $type ] ) && ! is_multisite() ) {
			return self::$get_user_templates_cache[ $type ];
		}
		$categorized = array(
			'uncategorized' => array(
				'name'      => _x( 'Uncategorized', 'Default user template category.', 'fl-builder' ),
				'templates' => array(),
			),
		);

		$posts = get_posts( array(
			'post_type'        => 'fl-builder-template',
			'orderby'          => 'menu_order title',
			'order'            => 'ASC',
			'posts_per_page'   => '-1',
			'suppress_filters' => false,
			'fields'           => 'ids',
			'post__not_in'     => [ self::get_post_id() ],
			'tax_query'        => array(
				array(
					'taxonomy' => 'fl-builder-template-type',
					'field'    => 'slug',
					'terms'    => $type,
				),
			),
		) );

		$templates = array();

		remove_filter( 'the_title', 'wptexturize' );
		remove_filter( 'the_title', 'convert_chars' );

		// Loop through templates posts and build the templates array.
		foreach ( $posts as $post ) {
			if ( has_post_thumbnail( $post ) ) {
				$image_data = wp_get_attachment_image_src( get_post_thumbnail_id( $post ), 'medium_large' );
				if ( is_array( $image_data ) ) {
					$image = $image_data[0];
				} else {
					$image = FLBuilder::plugin_url() . 'img/templates/blank.jpg';
				}
			} else {
				$image = FLBuilder::plugin_url() . 'img/templates/blank.jpg';
			}

			$templates[] = array(
				'id'               => get_post_meta( $post, '_fl_builder_template_id', true ),
				'postId'           => $post,
				'name'             => get_the_title( $post ),
				'image'            => $image,
				'kind'             => 'template',
				'type'             => 'user',
				'content'          => FLBuilderModel::get_user_template_type( $post ),
				'isGlobal'         => FLBuilderModel::is_post_global_node_template( $post ),
				'isDynamicEditing' => FLBuilderModel::is_post_dynamic_editing_node_template( $post ),
				'link'             => add_query_arg( 'fl_builder', '', get_permalink( $post ) ),
				'category'         => array(),
			);
		}

		add_filter( 'the_title', 'wptexturize' );
		add_filter( 'the_title', 'convert_chars' );

		// Loop through templates and build the categorized array.
		foreach ( $templates as $i => $template ) {

			$cats = get_the_terms( $template['postId'], 'fl-builder-template-category' );

			if ( ! $cats ) {
				$cats = array();
			}

			if ( 0 === count( $cats ) || is_wp_error( $cats ) ) {
				$template['category']                        = array(
					'uncategorized' => __( 'Uncategorized', 'fl-builder' ),
				);
				$categorized['uncategorized']['templates'][] = $template;
			} else {

				foreach ( $cats as $cat ) {
					$template['category'][ $cat->slug ] = $cat->name;
				}

				foreach ( $cats as $cat ) {
					if ( ! isset( $categorized[ $cat->slug ] ) ) {
						$categorized[ $cat->slug ] = array(
							'name'      => $cat->name,
							'templates' => array(),
						);
					}
					$categorized[ $cat->slug ]['templates'][] = $template;
				}
			}

			$templates[ $i ] = $template;
		}

		// Unset the uncategorized cat if no templates.
		if ( 0 === count( $categorized['uncategorized']['templates'] ) ) {
			unset( $categorized['uncategorized'] );
		}

		// sort the categories.
		asort( $categorized );
		self::$get_user_templates_cache[ $type ] = array(
			'templates'   => $templates,
			'categorized' => $categorized,
		);
		return self::$get_user_templates_cache[ $type ];
	}

	/**
	 * Returns the template type for a user template.
	 *
	 * @since 1.6.3
	 * @param int $template_id The post ID of the template.
	 * @return string
	 */
	static public function get_user_template_type( $template_id = null ) {
		if ( $template_id && isset( self::$node_template_types[ $template_id ] ) ) {
			return self::$node_template_types[ $template_id ];
		}

		$post = $template_id ? get_post( $template_id ) : FLBuilderModel::get_post();

		if ( ! is_object( $post ) || 'fl-builder-template' != $post->post_type ) {
			return '';
		} else {

			$terms = get_the_terms( $post->ID, 'fl-builder-template-type' );

			if ( ! $terms ) {
				$terms = array();
			}

			$type = ( is_wp_error( $terms ) || 0 === count( $terms ) ) ? 'layout' : $terms[0]->slug;

			if ( null !== $template_id ) {
				self::$node_template_types[ $template_id ] = $type;
			}

			return $type;
		}
	}

	/**
	 * Delete a user defined template.
	 *
	 * @since 1.1.3
	 * @param int $template_id The post ID of the template to delete.
	 * @return void
	 */
	static public function delete_user_template( $template_id = null ) {
		if ( isset( $template_id ) ) {
			wp_trash_post( $template_id, true );
		}
	}

	/**
	 * Apply a user defined template to the current layout.
	 *
	 * @since 1.1.3
	 * @param int|object $template The post ID of the template to apply or a template data object.
	 * @param bool $append Whether to append the new template or replacing the existing layout.
	 * @return array
	 */
	static public function apply_user_template( $template = null, $append = false ) {

		$data = array();
		if ( $template ) {

			// Delete existing nodes and settings?
			if ( ! $append ) {
				self::delete_layout_data( 'draft' );
				self::delete_layout_settings( 'draft' );
			}

			// Insert new nodes if this is not a blank template.
			if ( 'blank' != $template ) {

				// Get the template data if $template is not an object.
				if ( ! is_object( $template ) ) {
					$template_id        = $template;
					$template           = new StdClass();
					$template->nodes    = self::get_layout_data( 'published', $template_id );
					$template->settings = self::get_layout_settings( 'published', $template_id );
				}

				// Get new ids for the template nodes.
				$template->nodes = self::generate_new_node_ids( $template->nodes );

				// Set version numbers on modules if necessary.
				$template->nodes = FLBuilderModuleDeprecations::update_template_data_version( $template->nodes );

				// Get the existing layout data and settings.
				$layout_data     = self::get_layout_data();
				$layout_settings = self::get_layout_settings();

				// Reposition rows if we are appending.
				if ( $append ) {

					$row_position = self::next_node_position( 'row' );

					foreach ( $template->nodes as $node_id => $node ) {

						if ( 'row' == $node->type ) {
							$template->nodes[ $node_id ]->position += $row_position;
						}
					}
				}

				// Merge the layout data and settings.
				$data     = array_merge( $layout_data, $template->nodes );
				$settings = self::merge_layout_settings( $layout_settings, $template->settings );

				// Update the layout data and settings.
				self::update_layout_data( $data );
				self::update_layout_settings( $settings );

				// Delete old asset cache.
				self::delete_asset_cache();

			}
		}

		// Return the layout.
		return array(
			'layout_css' => isset( $settings ) ? $settings->css : null,
			'layout'     => FLBuilderAJAXLayout::render(),
			'config'     => FLBuilderUISettingsForms::get_node_js_config(),
			'newNodes'   => $data,
		);
	}

	/**
	 * Returns true if the node templates UI is enabled, false if not.
	 *
	 * @since 1.6.3
	 * @return bool
	 */
	static public function node_templates_enabled() {
		$enabled_templates = self::get_enabled_templates();

		if ( true === FL_BUILDER_LITE ) {
			return false;
		}
		if ( 'core' == $enabled_templates || 'disabled' == $enabled_templates ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks to see if the current post is a node template.
	 *
	 * @since 1.6.3
	 * @param int $post_id If supplied, this post will be checked instead.
	 * @param string $type If supplied, this node type will be checked instead of all types.
	 * @return bool
	 */
	static public function is_post_node_template( $post_id = false, $type = null ) {
		$post_id = $post_id ? $post_id : self::get_post_id();
		$post    = get_post( $post_id );

		if ( ! $post ) {
			return false;
		} elseif ( 'fl-builder-template' == $post->post_type ) {

			$saved_type = self::get_user_template_type( $post->ID );

			if ( $type ) {
				return $type === $saved_type;
			} elseif ( in_array( $saved_type, array( 'row', 'column', 'module' ) ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks to see if the current post is a global node template.
	 *
	 * @since 1.6.3
	 * @param int $post_id If supplied, this post will be checked instead.
	 * @return bool
	 */
	static public function is_post_global_node_template( $post_id = false ) {
		$post_id = $post_id ? $post_id : self::get_post_id();

		if ( ! self::is_post_node_template( $post_id ) ) {
			return false;
		}

		$global = get_post_meta( $post_id, '_fl_builder_template_global', true );

		if ( ! $global ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks to see if the current post is a global node template.
	 *
	 * @since 2.10
	 * @param int $post_id If supplied, this post will be checked instead.
	 * @return int
	 */
	static public function is_post_dynamic_editing_node_template( $post_id = false ) {
		if ( self::is_post_global_node_template( $post_id ) ) {
			return (int) get_post_meta( $post_id, '_fl_builder_template_dynamic_editing', true );
		}
		return 0;
	}

	/**
	 * Checks to see if the given node is dynamic global.
	 *
	 * @since 2.10
	 * @param object $node The node object to check.
	 * @return bool
	 */
	static public function is_node_dynamic( $node ) {
		if ( ! isset( $node->template_id ) ) {
			return false;
		}

		$template_post_id = self::get_node_template_post_id( $node->template_id );
		return (bool) self::is_post_dynamic_editing_node_template( $template_post_id );
	}

	/**
	 * Checks to see if a node is a global node.
	 *
	 * @since 1.6.3
	 * @param object $node The node object to check.
	 * @return bool|int
	 */
	static public function is_node_global( $node ) {
		if ( ! isset( $node->template_id ) ) {
			return false;
		}
		if ( empty( $node->template_id ) ) {
			return false;
		}
		return self::get_node_template_post_id( $node->template_id );
	}

	/**
	 * Check the visibility settings that has been sets from any type of node (rows/columns/modules)
	 * This will be applied ONLY when the builder is not active.
	 *
	 * @param object $node The type of object to check
	 * @return bool
	 */
	static public function is_node_visible( $node ) {
		global $wp_the_query;

		$is_visible = true;

		if ( isset( $node->settings->visibility_display ) && ( '' != $node->settings->visibility_display ) ) {

			// For logged out users
			if ( 'logged_out' == $node->settings->visibility_display && ! is_user_logged_in() ) {
				$is_visible = true;
			} elseif ( 'logged_in' == $node->settings->visibility_display && is_user_logged_in() ) {
				$is_visible = true;

				// User capability setting
				if ( isset( $node->settings->visibility_user_capability ) && ! empty( $node->settings->visibility_user_capability ) ) {
					if ( self::current_user_has_capability( trim( $node->settings->visibility_user_capability ) ) ) {
						$is_visible = true;
					} else {
						$is_visible = false;
					}
				}
			} elseif ( 0 == $node->settings->visibility_display ) {
				$is_visible = false;
			} else {
				$is_visible = false;
			}
		}

		return apply_filters( 'fl_builder_is_node_visible', $is_visible, $node );
	}

	/**
	 * Checks if a node has visibility rules or not.
	 *
	 * @param object $node
	 * @return bool
	 */
	static public function node_has_visibility_rules( $node ) {
		$rules = false;
		if ( isset( $node->settings->visibility_display ) && ( '' !== $node->settings->visibility_display ) ) {
			$rules = true;
		}
		if ( isset( $node->settings->responsive_display ) && ( '' !== $node->settings->responsive_display ) ) {
			$breakpoints = explode( ',', $node->settings->responsive_display );

			if ( count( $breakpoints ) < 4 ) {
				$rules = true;
			}
		}
		return $rules;
	}

	/**
	 * Returns visibility rule.
	 *
	 * @param object $node
	 * @return bool
	 */
	static public function node_visibility_rules( $node ) {

		$rule = isset( $node->settings->visibility_display ) ? $node->settings->visibility_display : '';
		$text = '';

		switch ( $rule ) {

			case 'logged_in':
				$text = __( 'Logged In', 'fl-builder' );
				break;

			case 'logged_out':
				$text = __( 'Logged Out', 'fl-builder' );
				break;

			case 'logic':
				$text = __( 'Logic', 'fl-builder' );
				break;

			default:
				$text = __( 'Breakpoint', 'fl-builder' );
		}

		return array(
			'text' => $text,
			'type' => $rule,
		);
	}

	/**
	 * Checks to see if a node is the root node of a global template.
	 *
	 * @since 1.6.3
	 * @param object $node The node object to check.
	 * @return bool|int
	 */
	static public function is_node_template_root( $node ) {
		return self::is_node_global( $node ) && isset( $node->template_root_node );
	}

	/**
	 * Get an array of node template info.
	 *
	 * @since 1.6.3
	 * @param string $type The type of node template to get.
	 * @return array
	 */
	static public function get_node_templates( $type = '' ) {
		$posts = get_posts( array(
			'post_type'      => 'fl-builder-template',
			'orderby'        => 'title',
			'order'          => 'ASC',
			'posts_per_page' => '-1',
			'tax_query'      => array(
				array(
					'taxonomy' => 'fl-builder-template-type',
					'field'    => 'slug',
					'terms'    => $type,
				),
			),
		) );

		$templates = array();

		foreach ( $posts as $post ) {

			$templates[] = array(
				'id'     => get_post_meta( $post->ID, '_fl_builder_template_id', true ),
				'global' => get_post_meta( $post->ID, '_fl_builder_template_global', true ),
				'link'   => add_query_arg( 'fl_builder', '', get_permalink( $post->ID ) ),
				'name'   => $post->post_title,
			);
		}

		return $templates;
	}

	/**
	 * Returns the root node for a node template.
	 *
	 * @since 1.6.3
	 * @param string $type The type of node template.
	 * @param array $nodes The node template data.
	 * @return object
	 */
	static public function get_node_template_root( $type = '', $nodes = array() ) {
		if ( '' != $type ) {
			$nodes = count( $nodes ) > 0 ? $nodes : self::get_nodes( $type );
		}

		if ( 'module' === $type && 1 === count( $nodes ) ) {
			return array_pop( $nodes );
		} else {
			foreach ( $nodes as $node ) {
				if ( $type === $node->type && ! $node->parent ) {
					return $node;
				}
			}
		}

		return false;
	}

	/**
	 * Uses a node template ID to retrieve its post ID.
	 *
	 * @since 1.6.3
	 * @param string $template_id The node template ID as stored in the template's post meta.
	 * @return int
	 */
	static public function get_node_template_post_id( $template_id ) {
		if ( isset( self::$node_template_post_ids[ $template_id ] ) ) {
			return self::$node_template_post_ids[ $template_id ];
		} else {

			$posts = get_posts( array(
				'post_type'      => 'fl-builder-template',
				'post_status'    => array( 'any', 'trash' ),
				'posts_per_page' => '-1',
				'post_status'    => 'any',
				'meta_key'       => '_fl_builder_template_id',
				'meta_value'     => $template_id,
			) );

			if ( 0 === count( $posts ) ) {
				return false;
			}

			$post_id                                      = apply_filters( 'fl_builder_node_template_post_id', $posts[0]->ID );
			self::$node_template_post_ids[ $template_id ] = $post_id;
			return $post_id;
		}
	}

	/**
	 * Returns the edit URL for a node template.
	 *
	 * @since 1.6.3
	 * @param string $template_id The node template ID as stored in the template's post meta.
	 * @return string
	 */
	static public function get_node_template_edit_url( $template_id ) {
		return self::get_edit_url( self::get_node_template_post_id( $template_id ) );
	}

	/**
	 * Returns an array of posts that have the global node template
	 * with the specified ID.
	 *
	 * @since 1.6.3
	 * @param int $post_id The post ID of the global node template.
	 * @return array
	 */
	static public function get_posts_with_global_node_template( $post_id = false ) {
		$posts = array();

		if ( self::is_post_global_node_template( $post_id ) ) {

			$post_types  = array_keys( get_post_types( [], 'names' ) );
			$template_id = get_post_meta( $post_id, '_fl_builder_template_id', true );

			$query = new WP_Query( array(
				'meta_query'     => array(
					'relation' => 'OR',
					array(
						'key'     => '_fl_builder_data',
						'value'   => $template_id,
						'compare' => 'LIKE',
					),
					array(
						'key'     => '_fl_builder_draft',
						'value'   => $template_id,
						'compare' => 'LIKE',
					),
				),
				'posts_per_page' => -1,
				'post_status'    => 'any',
				'post_type'      => $post_types,
				'post__not_in'   => array( $post_id ),
			) );

			$posts = $query->posts;
		}

		return $posts;
	}

	/**
	 * Saves a node template.
	 *
	 * @since 1.6.3
	 * @param string $template_node_id The ID of the node to save as a template.
	 * @param string $settings The settings for this template.
	 * @return void
	 */
	static public function save_node_template( $template_node_id, $settings ) {
		$root_node         = self::get_node( $template_node_id );
		$nodes             = self::get_nested_nodes( $template_node_id );
		$template_id       = self::generate_node_id();
		$original_parent   = $root_node->parent;
		$original_position = $root_node->position;
		$post_excerpt      = isset( $settings['notes'] ) ? wp_kses_post( $settings['notes'] ) : '';
		$dynamic_editing   = false;
		$global            = false;

		// Save the node template post.
		$post_id = wp_insert_post( array(
			'post_title'     => $settings['name'],
			'post_type'      => 'fl-builder-template',
			'post_status'    => 'publish',
			'ping_status'    => 'closed',
			'comment_status' => 'closed',
			'post_excerpt'   => $post_excerpt,
		) );

		// Set the template type.
		wp_set_post_terms( $post_id, $root_node->type, 'fl-builder-template-type' );

		// add extra optional data
		if ( isset( $settings['categories'] ) && ! empty( $settings['categories'] ) ) {
			if ( is_numeric( $settings['categories'] ) ) {
				wp_set_post_terms( $post_id, $settings['categories'], 'fl-builder-template-category' );
			} else {
				// new category or categories
				$cats = explode( ',', $settings['categories'] );
				foreach ( $cats as $cat ) {
					$check = term_exists( $cat, 'fl-builder-template-category' );

					if ( ! $check ) {
						$new_term = wp_insert_term( $cat, 'fl-builder-template-category' );
						$set      = wp_set_post_terms( $post_id, $new_term['term_id'], 'fl-builder-template-category', true );
					} else {
						wp_set_post_terms( $post_id, $check['term_id'], 'fl-builder-template-category', true );
					}
				}
			}
		}
		if ( isset( $settings['featured'] ) ) {
			add_post_meta( $post_id, '_thumbnail_id', $settings['featured'] );
		}

		// Reset the root node's position and parent.
		$root_node->position = 0;
		$root_node->parent   = null;

		// Reset column template width.
		if ( 'column' == $root_node->type ) {
			$root_node->settings->size = 100;
		}

		// Add the root node to the nodes array.
		$nodes[ $root_node->node ] = $root_node;

		// Generate new node ids.
		$nodes = self::generate_new_node_ids( $nodes );

		// Get the root node from the template data since its ID changed.
		$root_node = self::get_node_template_root( $root_node->type, $nodes );

		// Remove any child nodes that are a child of a global node in this template.
		// We don't need to store those references, only the root global node.
		$template_nodes = [];
		foreach ( $nodes as $node_id => $node ) {
			if ( isset( $node->template_id ) && ! isset( $node->template_root_node ) ) {
				continue;
			}
			$template_nodes[ $node_id ] = $node;
		}
		$nodes = $template_nodes;

		// Setup dynamic editing for components.
		if ( 'dynamic' === $settings['type'] ) {
			$dynamic_editing = true;
			$global          = true;
			update_post_meta( $post_id, '_fl_builder_template_dynamic_editing', $dynamic_editing );
		} elseif ( 'global' === $settings['type'] ) {
			$global = true;
		}

		// Add the template ID and template node ID for global templates.
		if ( $global ) {

			foreach ( $nodes as $node_id => $node ) {

				if ( false == $nodes[ $node_id ]->global ) {
					$nodes[ $node_id ]->template_id      = $template_id;
					$nodes[ $node_id ]->template_node_id = $node_id;
					$nodes[ $node_id ]->global           = $global;
					$nodes[ $node_id ]->dynamic          = $dynamic_editing;
					if ( $node_id == $root_node->node ) {
						$nodes[ $node_id ]->template_root_node = true;
					} elseif ( isset( $nodes[ $node_id ]->template_root_node ) ) {
						unset( $nodes[ $node_id ]->template_root_node );
					}
				}
			}
		}

		// Save the template layout data.
		self::update_layout_data( $nodes, 'published', $post_id );
		self::update_layout_data( $nodes, 'draft', $post_id );

		// Enable the builder for this template.
		update_post_meta( $post_id, '_fl_builder_enabled', true );

		// Add the template ID post meta. We use a custom ID for node
		// templates in case templates are imported since their WordPress
		// IDs will change, breaking global templates.
		update_post_meta( $post_id, '_fl_builder_template_id', $template_id );

		// Add the template global flag post meta.
		update_post_meta( $post_id, '_fl_builder_template_global', $global );

		// Delete the existing node and apply the template for global templates.
		if ( $global ) {

			// Delete the existing node.
			self::delete_node( $template_node_id );

			// Apply the global template.
			$root_node = self::apply_node_template( $template_id, $original_parent, $original_position );
		}

		// Return an array of template settings.
		return array(
			'id'                 => $template_id,
			'global'             => $global,
			'link'               => add_query_arg( 'fl_builder', '', get_permalink( $post_id ) ),
			'name'               => $settings['name'],
			'type'               => $root_node->type,
			'layout'             => $global ? FLBuilderAJAXLayout::render( $root_node->node, $template_node_id ) : null,
			'config'             => $global ? FLBuilderUISettingsForms::get_node_js_config() : null,
			'postID'             => $post_id,
			'template_id'        => $template_id,
			'template_node_id'   => $root_node->node,
			'template_root_node' => true,
			'dynamic_editing'    => $dynamic_editing,
			'parent'             => $original_parent,
			'position'           => $original_position,
			'settings'           => $root_node->settings,
		);
	}

	/**
	 * Sets the default type for a node template when created in wp-admin.
	 *
	 * @since 1.6.3
	 * @param int $post_ID The post ID for the template.
	 * @param object $post The post object for the template.
	 * @param bool $update Whether this is a new post or an update.
	 * @return void
	 */
	static public function set_node_template_default_type( $post_id, $post, $update ) {
		global $pagenow;

		if ( 'admin.php' == $pagenow && isset( $_GET['import'] ) ) {
			return;
		}

		$post_data = self::get_post_data();

		if ( $update || 'fl-builder-template' != $post->post_type ) {
			return;
		}
		if ( isset( $post_data['fl_action'] ) && 'duplicate_post' == $post_data['fl_action'] ) {
			return;
		}

		if ( isset( $_GET['duplicate_layout'] ) ) {
			return;
		}

		$type = wp_get_post_terms( $post_id, 'fl-builder-template-type' );

		if ( 0 === count( $type ) ) {
			wp_set_post_terms( $post_id, 'layout', 'fl-builder-template-type' );
		}
	}

	/**
	 * Deletes a node template via AJAX.
	 *
	 * @since 1.6.3
	 * @param string $template_id The ID of node template to delete.
	 * @return void
	 */
	static public function delete_node_template( $template_id ) {
		// Make sure we have a template ID.
		if ( ! isset( $template_id ) ) {
			return;
		}

		// Get the post ID for the template.
		$template_post_id = self::get_node_template_post_id( $template_id );

		// Bail if we don't have a post ID.
		if ( ! $template_post_id ) {
			return;
		}

		// Unlink if this is a global template.
		self::unlink_global_node_template_from_all_posts( $template_post_id );

		// Delete the template post.
		wp_trash_post( $template_post_id, true );
	}

	/**
	 * Duplicate a root global node in a layout, preserving all
	 * of the global references. This will only duplicate a root
	 * global node in a layout. It is not intended to duplicate
	 * children of global nodes.
	 *
	 * @since 2.10
	 * @param string $node_id
	 * @param string $parent_id
	 * @return object
	 */
	static public function duplicate_global_node( $node_id, $parent_id = null ) {
		$data = self::get_layout_data();

		$new_node_id  = self::generate_node_id();
		$new_position = $data[ $node_id ]->position + 1;

		$data[ $new_node_id ]       = unserialize( serialize( $data[ $node_id ] ) ); // Deep clone
		$data[ $new_node_id ]->node = $new_node_id;

		if ( null !== $parent_id ) {
			$data[ $new_node_id ]->parent = $parent_id;
		}

		self::update_layout_data( $data );
		self::reorder_node( $new_node_id, $new_position );

		return self::get_node( $new_node_id );
	}

	/**
	 * Unlink a single global node instance in a layout. This will remove
	 * the global reference from the node, turning it into a static node.
	 *
	 * @since 2.10
	 * @param string $node_id
	 * @return object
	 */
	static public function unlink_global_node( $node_id ) {
		$node = self::get_node( $node_id );

		if ( 'row' === $node->type ) {
			$new_node = self::copy_row( $node->node, null, null, false );
		} elseif ( 'column' === $node->type ) {
			$new_node = self::copy_col( $node->node, null, null, false );
		} elseif ( 'module' === $node->type ) {
			$new_node = self::copy_module( $node->node, null, null, null, false );
		}

		self::delete_node( $node->node );

		return self::get_node( $new_node->node );
	}

	/**
	 * Unlinks all instances of a global node template in all posts.
	 *
	 * @since 1.6.3
	 * @param int $template_post_id The post ID of the template to unlink.
	 * @return void
	 */
	static public function unlink_global_node_template_from_all_posts( $template_post_id ) {
		if ( self::is_post_global_node_template( $template_post_id ) ) {

			$posts       = self::get_posts_with_global_node_template( $template_post_id );
			$template_id = get_post_meta( $template_post_id, '_fl_builder_template_id', true );

			foreach ( $posts as $post ) {
				self::unlink_global_node_template_from_post( 'published', $post->ID, $template_post_id, $template_id );
				self::unlink_global_node_template_from_post( 'draft', $post->ID, $template_post_id, $template_id );
				self::delete_all_asset_cache( $post->ID );
			}
		}
	}

	/**
	 * Unlinks all instances of a global node template from a post's
	 * layout data with the specified status. Since only the root node
	 * of a global template is saved to a posts layout data, the child
	 * nodes will be saved to the post when the global template is unlinked.
	 *
	 * @since 1.6.3
	 * @param string $status The status of the layout data. Either draft or published.
	 * @param int $post_id The ID of the post to unlink from.
	 * @param string $template_post_id The post ID of the template to unlink from the layout data.
	 * @param string $template_id The ID of the template to unlink from the layout data.
	 * @return void
	 */
	static public function unlink_global_node_template_from_post( $status, $post_id, $template_post_id, $template_id ) {
		$template_data = self::get_layout_data( $status, $template_post_id );
		$layout_data   = self::get_layout_data( $status, $post_id );
		$update        = false;

		// Loop through the layout data.
		foreach ( $layout_data as $node_id => $node ) {

			// Check to see if this is the global template node to unlink.
			if ( isset( $node->template_id ) && $node->template_id == $template_id ) {

				// Clone the template data to avoid modifying the original.
				$new_data = [];
				foreach ( $template_data as $i => $n ) {
					$new_data[ $i ] = clone $n;
				}

				// Get the root node from the template data.
				$root_node = self::get_node_template_root( $node->type, $new_data );

				if ( is_object( $root_node ) ) {
					// Remove the root node from the template data since it's already in the layout.
					unset( $new_data[ $root_node->node ] );
				}

				// Merge children with dynamic settings if necessary.
				if ( $node->dynamic ) {
					foreach ( $new_data as $i => $n ) {
						$new_data[ $i ] = FLBuilderDynamicGlobal::merge_child_settings(
							$layout_data[ $node_id ],
							$new_data[ $i ],
							false
						);
					}
				}

				// Generate new node ids for the child template data.
				$new_data = self::generate_new_node_ids( $new_data );

				// Update the settings for the root node in this layout.
				if ( $node->dynamic ) {
					$layout_data[ $node_id ]->settings = FLBuilderDynamicGlobal::merge_settings_with_template(
						$layout_data[ $node_id ],
						$layout_data[ $node_id ]->settings
					);
				} else {
					if ( is_object( $root_node ) ) {
						$layout_data[ $node_id ]->settings = $root_node->settings;
					}
				}

				if ( is_object( $root_node ) ) {
					// Update children with the new parent node ID.
					foreach ( $new_data as $i => $n ) {
						if ( $n->parent == $root_node->node ) {
							$new_data[ $i ]->parent = $node_id;
						}
					}
				}

				// Add the template data to the layout data.
				$layout_data = array_merge( $layout_data, $new_data );

				// Set the update flag.
				$update = true;
			}
		}

		// Only update if we need to.
		if ( $update ) {

			// Get the template ID for the current post if it has one.
			$post_template_id = get_post_meta( $post_id, '_fl_builder_template_id', true );

			// Loop through the layout data and update the template ID.
			foreach ( $layout_data as $node_id => $node ) {

				// Check to see if this node is part of the global template to unlink.
				if ( isset( $node->template_id ) && $node->template_id == $template_id ) {

					// If this post has a template ID, set it to the new template ID for these nodes.
					// Otherwise, unset the template ID and remove dynamic settings.
					if ( $post_template_id ) {
						$layout_data[ $node_id ]->template_id      = $post_template_id;
						$layout_data[ $node_id ]->template_node_id = $node_id;
					} else {
						unset( $layout_data[ $node_id ]->template_id );
						unset( $layout_data[ $node_id ]->template_node_id );
						unset( $layout_data[ $node_id ]->settings->dynamic_fields );
						unset( $layout_data[ $node_id ]->settings->dynamic_node_settings );
					}

					// We're unlinking, so this is no longer a root node.
					unset( $layout_data[ $node_id ]->template_root_node );
				}
			}

			// Update the layout data.
			self::update_layout_data( $layout_data, $status, $post_id );
		}
	}

	/**
	 * Deletes all instances of a global node template from all posts.
	 *
	 * @since 1.6.3
	 * @param int $template_post_id The post ID of the template to delete.
	 * @return void
	 */
	static public function delete_global_node_template_from_all_posts( $template_post_id ) {
		if ( self::is_post_global_node_template( $template_post_id ) ) {

			$posts       = self::get_posts_with_global_node_template( $template_post_id );
			$template_id = get_post_meta( $template_post_id, '_fl_builder_template_id', true );

			foreach ( $posts as $post ) {
				self::delete_global_node_template_from_post( 'published', $post->ID, $template_id );
				self::delete_global_node_template_from_post( 'draft', $post->ID, $template_id );
				self::delete_all_asset_cache( $post->ID );
			}
		}
	}

	/**
	 * Deletes all instances of a global node template from a post's
	 * layout data with the specified status.
	 *
	 * @since 1.6.3
	 * @param string $status The status of the layout data. Either draft or published.
	 * @param int $post_id The ID of the post to delete from.
	 * @param string $template_id The ID of the template to delete from the layout data.
	 * @return void
	 */
	static public function delete_global_node_template_from_post( $status, $post_id, $template_id ) {
		$layout_data = self::get_layout_data( $status, $post_id );
		$update      = false;

		// Loop through the nodes.
		foreach ( $layout_data as $node_id => $node ) {

			$siblings = array();
			$position = 0;

			// Check to see if this is the global template node to delete.
			if ( isset( $node->template_id ) && $node->template_id == $template_id ) {

				// Unset this node in the layout data.
				unset( $layout_data[ $node_id ] );

				// Find sibling nodes to update their position.
				foreach ( $layout_data as $i => $n ) {
					if ( $n->parent == $node->parent ) {
						$siblings[ $i ] = $n;
					}
				}

				// Sort the sibling nodes by position.
				uasort( $siblings, array( 'FLBuilderModel', 'order_nodes' ) );

				// Update sibling node positions.
				foreach ( $siblings as $i => $n ) {
					$layout_data[ $i ]->position = $position;
					$position++;
				}

				// Set the update flag.
				$update = true;
			}
		}

		// Only update if we need to.
		if ( $update ) {
			self::update_layout_data( $layout_data, $status, $post_id );
		}
	}

	/**
	 * Applies a node template to the current layout.
	 *
	 * @since 1.6.3
	 * @param int $template_id The node template ID.
	 * @param string $parent_id The new parent node ID for the template.
	 * @param int $position The position of the template within the layout.
	 * @param object $template Optional. Template data to use instead of pulling it with the template ID.
	 * @return void
	 */
	static public function apply_node_template( $template_id = null, $parent_id = null, $position = 0, $template = null ) {
		$parent           = ( 0 === $parent_id ) ? null : self::get_node( $parent_id );
		$template_post_id = self::get_node_template_post_id( $template_id );
		$is_col_template  = false;

		/**
		 * Allow extensions to hook into applying a node template.
		 * @see fl_builder_override_apply_node_template
		 */
		$override = apply_filters( 'fl_builder_override_apply_node_template', false, array(
			'template_id'      => $template_id,
			'parent_id'        => $parent_id,
			'position'         => $position,
			'template'         => $template,
			'template_post_id' => $template_post_id,
		) );

		// Return if we got an override from the filter.
		if ( $override ) {
			return $override;
		}

		// Get the template data from $template if we have it.
		if ( is_object( $template ) ) {
			$template_data     = $template->nodes;
			$template_settings = $template->settings;
			$type              = $template->type;
			$global            = $template->global;
		} else {
			$template_data     = self::get_layout_data( 'published', $template_post_id );
			$template_settings = self::get_layout_settings( 'published', $template_post_id );
			$type              = self::get_user_template_type( $template_post_id );
			$global            = get_post_meta( $template_post_id, '_fl_builder_template_global', true );
		}

		// Filter the nodes for backwards compatibility with old settings.
		$template_data = FLBuilderSettingsCompat::filter_layout_data( $template_data );

		// Generate new node ids.
		$template_data = self::generate_new_node_ids( $template_data );

		// Set version numbers on modules if necessary.
		$template_data = FLBuilderModuleDeprecations::update_template_data_version( $template_data );

		// Get the root node from the template data.
		$root_node = self::get_node_template_root( $type, $template_data );

		// Handle module and column templates.
		if ( 'module' == $root_node->type || 'column' == $root_node->type ) {

			// Add a new parent for module or column node templates if needed.
			if ( ! $parent || 'row' == $parent->type || 'column-group' == $parent->type ) {

				if ( 'module' == $root_node->type ) {
					$module = self::get_module( $root_node );
					if ( ! $module->accepts_children() ) {
						$parent_id = self::add_module_parent( $root_node, $parent_id, $position );
						$position  = $parent_id ? null : $position;
					}
				} elseif ( 'column' == $root_node->type ) {
					$parent_id = self::add_col_parent( $parent_id, $position );
				}

				$parent = self::get_node( $parent_id );
			}

			// Set the global template references for all template nodes if the
			// parent is a global node and the template being applied is global.
			// This check allows globals to be applied to other globals.
			if ( self::is_node_global( $parent ) && ! self::is_node_global( $root_node ) ) {
				foreach ( $template_data as $node_id => $node ) {
					$template_data[ $node_id ]->template_id      = $parent->template_id;
					$template_data[ $node_id ]->template_node_id = $node_id;
				}
			}
		}

		// Update the root node's parent.
		$template_data[ $root_node->node ]->parent = ! $parent_id ? null : $parent_id;

		// Get the layout data and settings.
		$layout_data     = self::get_layout_data( 'draft' );
		$layout_settings = self::get_layout_settings( 'draft' );

		// Only merge the root node for global templates.
		if ( $global ) {
			$layout_data[ $root_node->node ] = $template_data[ $root_node->node ];
		} else {

			// Merge template data.
			$layout_data = array_merge( $layout_data, $template_data );

			// Merge template settings.
			$layout_settings = self::merge_layout_settings( $layout_settings, $template_settings );
		}

		if ( isset( $layout_data[ $root_node->node ]->settings->dynamic_fields ) ) {
			unset( $layout_data[ $root_node->node ]->settings->dynamic_fields );
		}

		// Update the layout data and settings.
		self::update_layout_data( $layout_data );
		self::update_layout_settings( $layout_settings );

		// Reorder the main template node.
		if ( null !== $position ) {
			self::reorder_node( $root_node->node, $position );
		}

		// Re-size column widths
		if ( 'column' == $root_node->type && 'column-group' == $parent->type ) {
			self::reset_col_widths( $parent_id );
		}

		// Delete old asset cache.
		self::delete_asset_cache();

		// Return the root node.
		if ( 'module' == $root_node->type ) {
			return self::get_module( $root_node->node );
		} else {
			return $root_node;
		}
	}

	/**
	 * Registers a template data file with the builder.
	 *
	 * @since 1.8
	 * @param string|array $path The directory path to the template data file.
	 * @param array $meta The collection information for this template file.
	 * @return void
	 */
	static public function register_templates( $path = false, $args = array() ) {
		// Check if the file exists if path is a string.
		if ( is_string( $path ) && ! file_exists( $path ) ) {
			return;
		}

		// Make sure one file exists if path is an array.
		if ( is_array( $path ) ) {

			$exists = false;

			foreach ( $path as $file ) {
				if ( file_exists( $file ) ) {
					$exists = true;
				}
			}

			if ( ! $exists ) {
				return;
			}
		}

		// Store the template data.
		self::$templates[] = array(
			'group' => isset( $args['group'] ) ? $args['group'] : false,
			'path'  => is_string( $path ) ? array( $path ) : $path,
		);
	}

	/**
	 * Registers the core templates with the builder.
	 *
	 * @since 1.10.3
	 * @return void
	 */
	static private function register_core_templates() {
		$templates = glob( FL_BUILDER_DIR . 'data/*' );
		$paths     = array();

		// glob() will return false on error so cast as an array() just in case.
		foreach ( (array) $templates as $template ) {
			$basename = basename( $template );

			if ( 'templates.dat' === $basename ) {
				continue;
			} elseif ( true !== FL_BUILDER_LITE && 'templates-config.dat' === $basename ) {
				continue;
			}

			$paths[] = $template;
		}

		self::register_templates( $paths );
	}

	/**
	 * Applies a core template and can be overridden by extensions to
	 * apply something else that is being shown in the selector.
	 *
	 * @since 1.0
	 * @since 1.5.7. Added logic for overriding core templates.
	 * @param int $index The index of the template to apply.
	 * @param bool $append Whether to append the new template or replacing the existing layout.
	 * @param string $type The type of template to apply.
	 * @return void
	 */
	static public function apply_template( $index = 0, $append = false, $type = 'layout' ) {
		/**
		 * Allow extensions to hook into applying a template.
		 * @see fl_builder_override_apply_template
		 */
		$override = apply_filters( 'fl_builder_override_apply_template', false, array(
			'index'  => $index,
			'append' => $append,
			'type'   => $type,
		) );

		// Return if we have an override from the filter.
		if ( $override ) {
			return $override;
		}

		// Apply a core template.
		return self::apply_core_template( $index, $append, $type );
	}

	/**
	 * Applies a core template and cannot be overridden by extensions.
	 *
	 * @since 1.10
	 * @param int $index The index of the template to apply.
	 * @param bool $append Whether to append the new template or replacing the existing layout.
	 * @param string $type The type of template to apply.
	 * @return array
	 */
	static public function apply_core_template( $index = 0, $append = false, $type = 'layout' ) {
		$template     = self::get_template( $index, $type );
		$row_position = self::next_node_position( 'row' );
		$data         = array();

		// Delete existing nodes and settings?
		if ( ! $append ) {
			self::delete_layout_data( 'draft' );
			self::delete_layout_settings( 'draft' );
		}

		// Only move forward if we have template nodes.
		if ( isset( $template->nodes ) ) {

			// Get new ids for the template nodes.
			$template->nodes = self::generate_new_node_ids( $template->nodes );

			// Filter the nodes for backwards compatibility with old settings.
			$template->nodes = FLBuilderSettingsCompat::filter_layout_data( $template->nodes );

			// Set version numbers on modules if necessary.
			$template->nodes = FLBuilderModuleDeprecations::update_template_data_version( $template->nodes );

			// Get the existing layout data and settings.
			$layout_data     = self::get_layout_data();
			$layout_settings = self::get_layout_settings();

			// Reposition rows?
			if ( $append ) {

				foreach ( $template->nodes as $node_id => $node ) {

					if ( 'row' == $node->type ) {
						$template->nodes[ $node_id ]->position += $row_position;
					}
				}
			}

			// Merge and update the layout data.
			$data = array_merge( $layout_data, $template->nodes );
			self::update_layout_data( $data );

			// Merge and update the layout settings.
			if ( isset( $template->settings ) ) {
				$settings = self::merge_layout_settings( $layout_settings, $template->settings );
				self::update_layout_settings( $settings );
			}
		}

		// Delete old asset cache.
		self::delete_asset_cache();

		// Return the layout.
		return array(
			'layout'   => FLBuilderAJAXLayout::render(),
			'config'   => FLBuilderUISettingsForms::get_node_js_config(),
			'newNodes' => $data,
		);
	}

	/**
	 * Returns data for a core template.
	 *
	 * @since 1.0
	 * @param int $index The index of the template.
	 * @param string $type The type of template to get. Currently either layout, row or module.
	 * @return object
	 */
	static public function get_template( $index, $type = 'layout' ) {
		$templates = self::get_templates( $type );
		$template  = isset( $templates[ $index ] ) ? $templates[ $index ] : false;

		if ( $template && isset( $template->nodes ) ) {
			$template->nodes = maybe_unserialize( $template->nodes );
		}

		return $template;
	}

	/**
	 * Returns data for all core or third party templates.
	 *
	 * @since 1.0
	 * @param string $type Either layout, row or module
	 * @param bool $cached
	 * @return array
	 */
	static public function get_templates( $type = 'layout', $cached = true ) {
		// Pull from dat files if cached is false or we don't have saved data.
		if ( ! $cached || ! self::$template_data ) {

			self::$template_data = array();

			foreach ( self::$templates as $args ) {

				foreach ( $args['path'] as $path ) {

					// Make sure the template file exists.
					if ( ! file_exists( $path ) ) {
						continue;
					}

					// Get the unserialized template data.
					if ( stristr( $path, '.php' ) ) {
						ob_start();
						include $path;
						$unserialized = unserialize( ob_get_clean() );
					} else {
						$unserialized = fl_maybe_fix_unserialize( file_get_contents( $path ) );
					}

					// Make sure we have an unserialized array.
					if ( ! is_array( $unserialized ) ) {
						continue;
					}

					// Group and cache the template data.
					foreach ( $unserialized as $template_type => $template_data ) {

						if ( ! isset( self::$template_data[ $template_type ] ) ) {
							self::$template_data[ $template_type ] = array();
						}

						foreach ( $template_data as $key => $template ) {

							// Add the main group to each template.
							if ( ! isset( $template_data[ $key ]->group ) ) {
								$template_data[ $key ]->group = $args['group'];
							}

							// Reserialize the node data as it's expensive to store in memory.
							if ( isset( $template->nodes ) ) {
								$template_data[ $key ]->nodes = serialize( $template_data[ $key ]->nodes );
							}
						}
						self::$template_data[ $template_type ] = array_merge( self::$template_data[ $template_type ], $template_data );
					}
				}
			}
		}

		$templates = isset( self::$template_data[ $type ] ) ? self::$template_data[ $type ] : array();

		ksort( $templates );

		/**
		 * @see fl_builder_get_templates
		 */
		return apply_filters( 'fl_builder_get_templates', $templates, $type );
	}

	/**
	 * Checks to see if any templates exist.
	 *
	 * @since 1.8
	 * @return bool
	 */
	static public function has_templates() {
		return apply_filters( 'fl_builder_has_templates', ( count( self::get_templates() ) > 0 ) );
	}

	/**
	 * Returns template data needed for the template selector.
	 * Can also return data for row, column and module templates if
	 * a template type is passed.
	 *
	 * @since 1.5.7
	 * @param string $type Either layout, row or module
	 * @return array
	 */
	static public function get_template_selector_data( $type = 'layout' ) {
		$type        = apply_filters( 'fl_builder_template_selector_data_type', $type );
		$categorized = array();
		$templates   = array();
		$groups      = array();

		// This is needed for backwards compat with the old core templates category.
		$core_categories = array(
			'general' => 'General',
			'landing' => 'Landing Pages',
			'company' => 'Content Pages',
		);

		// Build the the templates array.
		foreach ( self::get_templates( $type ) as $key => $template ) {

			if ( 'module' == $type && isset( $template->nodes ) ) {

				$nodes = maybe_unserialize( $template->nodes );
				$node  = array_shift( $nodes );

				if ( ! isset( $node->settings ) || ! isset( self::$modules[ $node->settings->type ] ) ) {
					continue;
				}
			}

			if ( strstr( $template->image, '://' ) || strstr( $template->image, ';base64,' ) ) {
				$image = $template->image;
			} else {
				$image = FLBuilder::plugin_url() . 'img/templates/' . ( empty( $template->image ) ? 'blank.jpg' : $template->image );
			}

			$templates[] = apply_filters( 'fl_builder_template_details', array(
				'id'       => $key,
				'name'     => $template->name,
				'image'    => $image,
				'author'   => '',
				'category' => isset( $template->category ) ? $template->category : $template->categories,
				'tags'     => array(),
				'group'    => $template->group,
				'type'     => 'core',
				'subtype'  => $type,
				'kind'     => 'template',
				'content'  => ! in_array( $type, array( 'row', 'column', 'module' ) ) ? 'layout' : $type,
				'premium'  => isset( $template->premium ) ? ! ! $template->premium : false,
			), $template );
		}

		// Build the categorized templates array and groups array.
		foreach ( $templates as $i => $template ) {

			// Make sure we have a template category and it's an array.
			if ( ! isset( $template['category'] ) ) {
				$template['category'] = array(
					'uncategorized' => __( 'Uncategorized', 'fl-builder' ),
				);
			} elseif ( is_string( $template['category'] ) ) {
				$template['category'] = array(
					$template['category'] => $core_categories[ $template['category'] ],
				);
			}

			// Get template group data.
			$template_groups = array();

			if ( ! $template['group'] ) {
				// If we don't have a group, use categories as groups.
				foreach ( $template['category'] as $cat_name ) {
					$template_groups[] = __( $cat_name, 'fl-builder' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
				}
				// Clear the categories since we're using groups instead.
				$template['category'] = array(
					'none' => '',
				);

			} elseif ( is_string( $template['group'] ) ) {
				// Make sure template group is an array.
				$template_groups = array( $template['group'] );
			} else {
				$template_groups = $template['group'];
			}

			// Add to the groups array.
			$template['group'] = array();

			foreach ( $template_groups as $group_name ) {
				$group_key = sanitize_key( $group_name );
				if ( ! isset( $groups[ $group_key ] ) ) {
					$groups[ $group_key ] = array(
						'name'       => $group_name,
						'categories' => array(),
					);
				}
				foreach ( $template['category'] as $cat_key => $cat_name ) {
					if ( ! isset( $groups[ $group_key ]['categories'][ $cat_key ] ) ) {
						$groups[ $group_key ]['categories'][ $cat_key ] = array(
							'name' => $cat_name,
						);
					}
				}
				ksort( $groups[ $group_key ]['categories'] );
				$template['group'][] = $group_key;
			}

			// Add to the categorized array.
			foreach ( $template['category'] as $cat_key => $cat_name ) {

				// Add the category if we don't have it yet.
				if ( ! isset( $categorized[ $cat_key ] ) ) {
					$categorized[ $cat_key ] = array(
						'name'      => $cat_name,
						'templates' => array(),
					);
				}

				$categorized[ $cat_key ]['templates'][] = $template;
			}

			$templates[ $i ] = $template;
		}

		/**
		 * Return both the templates and categorized templates array.
		 * @see fl_builder_template_selector_data
		 */
		return apply_filters( 'fl_builder_template_selector_data', array(
			'templates'   => $templates,
			'categorized' => $categorized,
			'groups'      => $groups,
		), $type );
	}

	/**
	 * Returns data for row templates to be shown in the UI panel.
	 *
	 * @since 1.8
	 * @return array
	 */
	static public function get_row_templates_data() {
		return apply_filters( 'fl_builder_row_templates_data', self::get_template_selector_data( 'row' ) );
	}

	/**
	 * Returns data for column templates to be shown in the UI panel.
	 *
	 * @since 2.1
	 * @return array
	 */
	static public function get_column_templates_data() {
		return apply_filters( 'fl_builder_column_templates_data', self::get_template_selector_data( 'column' ) );
	}

	/**
	 * Returns data for module templates to be shown in the UI panel.
	 *
	 * @since 1.8
	 * @return array
	 */
	static public function get_module_templates_data() {
		return apply_filters( 'fl_builder_module_templates_data', self::get_template_selector_data( 'module' ) );
	}

	/**
	 * Returns the config for pro modules if it exists.
	 *
	 * @since 2.4
	 * @return object
	 */
	static public function get_pro_modules_config() {
		$path   = FL_BUILDER_DIR . 'json/modules-config.json';
		$config = new stdClass();

		if ( file_exists( $path ) ) {
			$config = json_decode( file_get_contents( $path ) );
		}

		return $config;
	}

	/**
	 * Get color presets.
	 *
	 * @since 1.6.4
	 * @return object
	 */
	static public function get_color_presets() {
		$settings = get_option( '_fl_builder_color_presets', array() );

		return apply_filters( 'fl_builder_color_presets', $settings );
	}

	/**
	 * Save color presets.
	 *
	 * @since 1.6.4
	 * @param array $presets The new color presets collection.
	 * @return object
	 */
	static public function save_color_presets( $presets = array() ) {
		return FLBuilderUtils::update_option( '_fl_builder_color_presets', $presets );
	}

	/**
	 * Returns whether the UI has been white labeled or not.
	 *
	 * @since 2.1
	 * @return bool
	 */
	static public function is_white_labeled() {
		if ( class_exists( 'FLBuilderWhiteLabel' ) ) {
			return FLBuilderWhiteLabel::is_white_labeled();
		}

		return false;
	}

	/**
	 * Returns whether the inline editing is enabled.
	 *
	 * @since 2.1
	 * @return bool
	 */
	static public function is_inline_enabled() {
		return apply_filters( 'fl_inline_editing_enabled', true );
	}

	/**
	 * Returns whether the Ace Editor error checking is enabled.
	 *
	 * @since 2.1
	 * @return bool
	 */
	static public function is_codechecking_enabled() {

		/**
		 * Is code checking enabled?
		 * @see fl_code_checking_enabled
		 */
		$enabled = apply_filters( 'fl_code_checking_enabled', true );
		/**
		 * Enable shortcodes in css/js
		 * @see fl_enable_shortcode_css_js
		 * @since 2.3
		 */
		if ( true === apply_filters( 'fl_enable_shortcode_css_js', false ) ) {
			$enabled = false;
		}
		return $enabled;
	}

	/**
	 * Returns Ace Editor defaults as an array.
	 *
	 * @since 2.1
	 * @return array
	 */
	static public function ace_editor_settings() {

		$defaults = array(
			'enableBasicAutocompletion' => true,
			'enableLiveAutocompletion'  => true,
			'enableSnippets'            => false,
			'showLineNumbers'           => true,
			'wrap'                      => true,
			'showFoldWidgets'           => false,
		);
		/**
		 * Default Ace editor settings
		 * @see fl_ace_editor_settings
		 * @since 2.1
		 */
		return apply_filters( 'fl_ace_editor_settings', $defaults );
	}

	/**
	 * Returns the custom branding string.
	 *
	 * @since 1.3.1
	 * @return string
	 */
	static public function get_branding() {
		if ( class_exists( 'FLBuilderWhiteLabel' ) ) {
			return FLBuilderWhiteLabel::get_branding();
		}

		return __( 'Beaver Builder', 'fl-builder' );
	}

	/**
	 * Returns the custom branding icon URL.
	 *
	 * @since 1.3.7
	 * @return string
	 */
	static public function get_branding_icon() {
		if ( class_exists( 'FLBuilderWhiteLabel' ) ) {
			return FLBuilderWhiteLabel::get_branding_icon();
		}

		return FLBuilder::plugin_url() . 'img/beaver.png';
	}

	/**
	 * Returns an array of slugs for all enabled icon sets.
	 *
	 * @since 1.4.6
	 * @return array
	 */
	static public function get_enabled_icons() {
		$value = self::get_admin_settings_option( '_fl_builder_enabled_icons', true );

		/**
		 * font-awesome should not be a key in this array, if it is it can cause issues.
		 */
		if ( is_array( $value ) ) {
			$key = array_search( 'font-awesome', $value, true );
			if ( false !== $key ) {
				unset( $value[ $key ] );
			}
		}
		return ! $value ? array( 'font-awesome-5-regular', 'font-awesome-5-solid', 'font-awesome-5-brands', 'foundation-icons', 'dashicons' ) : $value;
	}

	/**
	 * Check if the current user has the specific capabilities
	 *
	 * @param string $cap   The capability to evaluate if it's single or multiple (comma separated) value
	 * @return bool
	 */
	static public function current_user_has_capability( $cap ) {
		if ( strstr( $cap, ',' ) ) {

			$parts = explode( ',', $cap );

			foreach ( $parts as $part ) {
				if ( current_user_can( trim( $part ) ) ) {
					return true;
				}
			}

			return false;
		} else {
			return current_user_can( $cap );
		}
	}

	/**
	 * Returns the default settings for the builder's help button.
	 *
	 * @since 1.4.9
	 * @return array
	 */
	static public function get_help_button_defaults() {
		$defaults = array(
			'enabled'            => true,
			'tour'               => apply_filters( 'fl_builder_tour_enabled', true ),
			'video'              => true,
			'video_embed'        => '<iframe src="https://player.vimeo.com/video/240550556?autoplay=1" width="420" height="315" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
			'knowledge_base'     => true,
			'knowledge_base_url' => self::get_store_url( 'knowledge-base', array(
				'utm_medium'   => ( true === FL_BUILDER_LITE ? 'bb-lite' : 'bb-pro' ),
				'utm_source'   => 'builder-ui',
				'utm_campaign' => 'kb-help-button',
			) ),
			'forums'             => true,
			'forums_url'         => self::get_store_url( 'knowledge-base', array(
				'utm_medium'   => ( true === FL_BUILDER_LITE ? 'bb-lite' : 'bb-pro' ),
				'utm_source'   => 'builder-ui',
				'utm_campaign' => 'forums-help-button',
			) ),
		);

		return $defaults;
	}

	/**
	 * Returns the settings for the builder's help button.
	 *
	 * @since 1.4.9
	 * @return array
	 */
	static public function get_help_button_settings() {
		if ( class_exists( 'FLBuilderWhiteLabel' ) ) {
			return FLBuilderWhiteLabel::get_help_button_settings();
		}

		return self::get_help_button_defaults();
	}

	/**
	 * Get row resize settings
	 *
	 * @since 2.0
	 * @return array
	 */
	static public function get_row_resize_settings() {
		$defaults = array(
			'userCanResizeRows' => true,
			'minAllowedWidth'   => 300,
			'maxAllowedWidth'   => false,
		);

		$settings = apply_filters( 'fl_row_resize_settings', $defaults );

		// Ensure everything is still defined after filter
		$settings = wp_parse_args( $settings, $defaults );

		// Min width can't go lower than 100px
		if ( false == $settings['minAllowedWidth'] || $settings['minAllowedWidth'] < 100 ) {
			$settings['minAllowedWidth'] = 100;
		}

		// Convert string numbers to int
		if ( is_string( $settings['minAllowedWidth'] ) ) {
			$settings['minAllowedWidth'] = intval( $settings['minAllowedWidth'] );
		}
		if ( is_string( $settings['maxAllowedWidth'] ) ) {
			$settings['maxAllowedWidth'] = intval( $settings['maxAllowedWidth'] );
		}

		// Check user capability
		if ( ! FLBuilderUserAccess::current_user_can( 'unrestricted_editing' ) ) {
			$settings['userCanResizeRows'] = false;
		}

		return $settings;
	}

	/**
	* Filter the row settings to remove max width field
	*
	* @since 2.0
	* @return array
	*/
	static public function filter_row_settings_for_resize( $form, $id ) {

		if ( 'row' == $id && ! FLBuilderModel::user_can_resize_rows() ) {
			unset( $form['tabs']['style']['sections']['general']['fields']['max_content_width'] );
		}
		return $form;
	}

	/**
	 * Check if user has the ability to resize rows
	 *
	 * @since 2.0
	 * @return bool
	 */
	static public function user_can_resize_rows() {
		$args = self::get_row_resize_settings();
		return $args['userCanResizeRows'];
	}

	/**
	 * Returns an array of account data for all integrated services.
	 *
	 * @since 1.5.4
	 * @return array
	 */
	static public function get_services() {
		return get_option( '_fl_builder_services', array() );
	}

	/**
	 * Updates the account data for an integrated service.
	 *
	 * @since 1.5.4
	 * @param string $service The service id.
	 * @param string $account The account name.
	 * @param array $data The account data.
	 * @return void
	 */
	static public function update_services( $service, $account, $data ) {
		$services = self::get_services();
		$account  = sanitize_text_field( $account );

		if ( ! isset( $services[ $service ] ) ) {
			$services[ $service ] = array();
		}

		$services[ $service ][ $account ] = $data;

		FLBuilderUtils::update_option( '_fl_builder_services', $services );
	}

	/**
	 * Deletes an account for an integrated service.
	 *
	 * @since 1.5.4
	 * @param string $service The service id.
	 * @param string $account The account name.
	 * @return void
	 */
	static public function delete_service_account( $service, $account ) {
		$services = self::get_services();

		if ( isset( $services[ $service ][ $account ] ) ) {
			unset( $services[ $service ][ $account ] );
		}
		if ( 0 === count( $services[ $service ] ) ) {
			unset( $services[ $service ] );
		}

		FLBuilderUtils::update_option( '_fl_builder_services', $services );
	}

	/**
	 * Returns an option from the database for
	 * the admin settings page.
	 *
	 * @since 1.5.7
	 * @param string $key The option key.
	 * @param bool $network_override Whether to allow the network admin setting to be overridden on subsites.
	 * @return mixed
	 */
	static public function get_admin_settings_option( $key, $network_override = true ) {
		if ( is_network_admin() ) {
			// Get the site-wide option if we're in the network admin.
			$value = get_site_option( $key );
		} elseif ( ! $network_override && class_exists( 'FLBuilderMultisiteSettings' ) ) {
			// Get the site-wide option if there's no network override.
			$value = get_site_option( $key );
		} elseif ( class_exists( 'FLBuilderMultisiteSettings' ) ) {
			// Network overrides are allowed. Return the subsite option if it exists.
			$value = get_option( $key );
			$value = false === $value ? get_site_option( $key ) : $value;
		} else {
			// This must be a single site install. Get the single site option.
			$value = get_option( $key );
		}

		return $value;
	}

	/**
	 * Updates an option from the admin settings page.
	 *
	 * @since 1.5.7
	 * @param string $key The option key.
	 * @param mixed $value The value to update.
	 * @param bool $network_override Whether to allow the network admin setting to be overridden on subsites.
	 * @return mixed
	 */
	static public function update_admin_settings_option( $key, $value, $network_override = true, $autoload = false ) {
		if ( is_network_admin() ) {
			// Update the site-wide option since we're in the network admin.
			update_site_option( $key, $value );
		} elseif ( $network_override && FLBuilderAdminSettings::multisite_support() && ! isset( $_POST['fl-override-ms'] ) ) {
			// Delete the option if we don't have a network override.
			delete_option( $key );
		} else {
			// Update the option for single install or subsite.
			FLBuilderUtils::update_option( $key, $value, $autoload );
		}
	}

	/**
	 * Returns the plugin basename for Beaver Builder.
	 *
	 * @since 1.0
	 * @return string
	 */
	static public function plugin_basename() {
		return plugin_basename( FL_BUILDER_DIR . 'fl-builder.php' );
	}

	/**
	 * Deletes almost all database data and asset cache for the builder.
	 * We don't delete _fl_builder_enabled, _fl_builder_data and _fl_builder_draft
	 * so layouts can be recovered should the plugin be installed again.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function uninstall_database() {
		if ( current_user_can( 'delete_plugins' ) ) {

			// Delete builder options.
			delete_option( '_fl_builder_settings' );
			delete_option( '_fl_builder_enabled_modules' );
			delete_option( '_fl_builder_enabled_templates' );
			delete_option( '_fl_builder_templates_override' );
			delete_option( '_fl_builder_templates_override_rows' );
			delete_option( '_fl_builder_templates_override_columns' );
			delete_option( '_fl_builder_templates_override_modules' );
			delete_option( '_fl_builder_post_types' );
			delete_option( '_fl_builder_enabled_icons' );
			delete_option( '_fl_builder_branding' );
			delete_option( '_fl_builder_branding_icon' );
			delete_option( '_fl_builder_theme_branding' );
			delete_option( '_fl_builder_user_access' );
			delete_option( '_fl_builder_help_button' );
			delete_option( '_fl_builder_color_presets' );

			// Delete builder user meta.
			delete_metadata( 'user', 0, '_fl_builder_launched', 1, true );

			// Delete uploaded files and folders.
			$upload_dir = self::get_upload_dir();
			fl_builder_filesystem()->rmdir( $upload_dir['path'], true );

			// Deactivate and delete the plugin.
			if ( ! function_exists( 'deactivate_plugins' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}
			deactivate_plugins( array( self::plugin_basename() ), false, is_network_admin() );
			delete_plugins( array( self::plugin_basename() ) );

			// Redirect to the plugins page.
			wp_redirect( admin_url( 'plugins.php?deleted=true&plugin_status=all&paged=1&s=' ) );

			exit;
		}
	}

	/**
	 * Be sure to return a unique string
	 * @since 2.5
	 */
	static public function uniqid( $prefix = '', $length = 12 ) {
		$id = substr( str_shuffle( '0123456789abcdefghijklmnopqrstuvwxyz' ), 0, $length );
		if ( preg_match( '/^[0-9]+$/', $id ) ) {
			$id = self::uniqid( $prefix, $length );
		}
		return ( $prefix ) ? $prefix . '-' . $id : $id;
	}

	/**
	 * @since 2.6
	 */
	static public function user_has_unfiltered_html() {
		return apply_filters( 'fl_user_has_unfiltered_html', current_user_can( 'unfiltered_html' ) );
	}

	/**
	 * @since 1.6.4.3
	 * @deprecated 1.8
	 */
	static public function get_theme_branding() {
		_deprecated_function( __METHOD__, '1.8', 'FLBuilderWhiteLabel::get_theme_branding()' );

		if ( class_exists( 'FLBuilderWhiteLabel' ) ) {
			return FLBuilderWhiteLabel::get_theme_branding();
		}
	}

	/**
	 * @since 1.0
	 * @deprecated 1.8
	 */
	static public function save_templates( $templates = array() ) {
		_deprecated_function( __METHOD__, '1.8', 'FLBuilderCoreTemplatesAdmin::save_templates()' );

		if ( class_exists( 'FLBuilderCoreTemplatesAdmin' ) ) {
			FLBuilderCoreTemplatesAdmin::save_templates( $templates );
		}
	}

	/**
	 * @since 1.0
	 * @deprecated 1.8
	 */
	static public function save_template( $settings ) {
		_deprecated_function( __METHOD__, '1.8', 'FLBuilderCoreTemplatesAdmin::save_template()' );

		if ( class_exists( 'FLBuilderCoreTemplatesAdmin' ) ) {
			FLBuilderCoreTemplatesAdmin::save_template( $settings );
		}
	}

	/**
	 * @since 1.0
	 * @deprecated 1.8
	 */
	static public function update_template( $old_index, $settings ) {
		_deprecated_function( __METHOD__, '1.8', 'FLBuilderCoreTemplatesAdmin::update_template()' );

		if ( class_exists( 'FLBuilderCoreTemplatesAdmin' ) ) {
			FLBuilderCoreTemplatesAdmin::update_template( $old_index, $settings );
		}
	}

	/**
	 * @since 1.0
	 * @deprecated 1.8
	 */
	static public function delete_template( $index ) {
		_deprecated_function( __METHOD__, '1.8', 'FLBuilderCoreTemplatesAdmin::delete_template()' );

		if ( class_exists( 'FLBuilderCoreTemplatesAdmin' ) ) {
			FLBuilderCoreTemplatesAdmin::delete_template( $index );
		}
	}

	/**
	 * @since 1.3.9
	 * @deprecated 1.10
	 */
	static public function get_editing_capability() {
		_deprecated_function( __METHOD__, '1.10' );

		return 'edit_posts';
	}

	/**
	 * @since 1.7
	 * @deprecated 1.10
	 */
	static public function current_user_has_editing_capability() {
		_deprecated_function( __METHOD__, '1.10', 'FLBuilderUserAccess::current_user_can()' );

		return FLBuilderUserAccess::current_user_can( 'unrestricted_editing' );
	}

	/**
	 * @since 1.6.3
	 * @deprecated 1.10
	 */
	static public function get_global_templates_editing_capability() {
		_deprecated_function( __METHOD__, '1.10', 'FLBuilderUserAccess::current_user_can' );

		return 'edit_posts';
	}

	/**
	 * @since 1.5.7
	 * @deprecated 1.10
	 */
	static public function user_templates_admin_enabled() {
		_deprecated_function( __METHOD__, '1.10', 'FLBuilderUserAccess::current_user_can( "builder_admin" )' );

		return FLBuilderUserAccess::current_user_can( 'builder_admin' );
	}

	/**
	 * @since 1.0
	 * @deprecated 2.0
	 */
	static public function get_module_category_slug( $name ) {
		_deprecated_function( __METHOD__, '2.0' );

		return sanitize_html_class( $name );
	}

	/**
	 * @since 1.8
	 * @deprecated 2.0
	 */
	static public function get_template_selector_filter_data() {
		_deprecated_function( __METHOD__, '2.0' );

		return array();
	}
}

FLBuilderModel::init();
