<?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 that 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();

	/**
	 * 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('before_delete_post',                               __CLASS__ . '::delete_post');
		add_action('save_post',                                        __CLASS__ . '::save_revision');
		add_action('save_post',                                        __CLASS__ . '::set_node_template_default_type', 10, 3);
		add_action('wp_restore_post_revision',                         __CLASS__ . '::restore_revision', 10, 2);

		/* Filters */
		add_filter('heartbeat_received',                               __CLASS__ . '::lock_post', 10, 2);

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

	/**
	 * Returns a builder edit URL for a post.
	 *
	 * @since 1.0
	 * @param int $post_id The post id to get an edit url for.
	 * @return string
	 */
	static public function get_edit_url( $post_id = false )
	{
		if ( false === $post_id ) {
			global $post;
		}
		else {
			$post = get_post( $post_id );
		}

		return set_url_scheme( add_query_arg( 'fl_builder', '', get_permalink( $post->ID ) ) );
	}

	/**
	 * 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() )
	{
		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 = trailingslashit( FL_BUILDER_STORE_URL . $path ) . '?' . http_build_query( $params, '', '&' );

		return apply_filters( 'fl_builder_store_url', $url, $path );
	}

	/**
	 * 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'] );
				}

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

				foreach($data as $key => $val) {
					self::$post_data[$key] = $val;
				}
			}
			else if(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';
		}

		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()
	{
		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
	 * @return int|bool The post id or false.
	 */
	static public function get_post_id()
	{
		global $wp_the_query;
		global $post;

		$post_data = self::get_post_data();

		// Get a post ID from the internal $post_id array if not empty.
		if ( ! empty( self::$post_id ) ) {
			return self::$post_id[ 0 ];
		}
		// Get a post ID sent in an AJAX request.
		else if ( isset( $post_data['post_id'] ) ) {
			return $post_data['post_id'];
		}
		// Get a post ID from the main query.
		else if ( in_the_loop() && is_main_query() && isset( $wp_the_query->post ) ) {
			return $wp_the_query->post->ID;
		}
		// Get a post ID in a query outside of the main loop.
		else if ( isset( $post ) ) {
			return $post->ID;
		}
		// No post ID found.
		else {
			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;
		}
		else if ( 0 === stripos( get_option( 'siteurl' ), 'https://' ) ) {
			return true;
		}
		else if ( 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 );

			if ( in_array( $post->post_type, $post_types ) && $user_can ) {
				$editable = true;
			}
		}

		return (bool) apply_filters( 'fl_builder_is_post_editable', $editable );
	}

	/**
	 * 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']);
		}
	}

	/**
	 * Checks to see if the builder layout is enabled
	 * for the current post.
	 *
	 * @since 1.0
	 * @return bool
	 */
	static public function is_builder_enabled()
	{
		$post_id = self::get_post_id();

		if(!is_admin() && post_password_required($post_id)) {
			return false;
		}
		else if(self::is_builder_active()) {
			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 self::$active;
		}
		else if ( ! is_admin() && is_singular() && $query_id != $post_id ) {
			self::$active = false;
		}
		else if ( 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 self::$active;
	}

	/**
	 * 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() ) {

			$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()
	{
		return self::is_builder_active() ? 'draft' : 'published';
	}

	/**
	 * 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()
	{
		update_post_meta(self::get_post_id(), '_fl_builder_enabled', false);
	}

	/**
	 * 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' );

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

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

				self::add_module( 'rich-text', $settings, $col->node );
			}
			// Create a new draft?
			else if ( 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.
			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();
		$dir_name = basename( FL_BUILDER_DIR );

		// We use bb-plugin for the lite version as well.
		if ( $dir_name == 'beaver-builder-lite-version' ) {
			$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 ( ! file_exists( $dir_info['path'] ) ) {

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

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

		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( ! file_exists( $dir_info['path'] ) ) {

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

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

		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()
	{
		$post_id = self::get_post_id();
		$active	 = self::is_builder_active();

		if($active) {
			return md5(uniqid());
		}
		else {
			return 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();

		if(isset($post_data['node_preview'])) {
			$suffix = '-layout-preview';
		}
		else if(self::is_builder_active()) {
			$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;
	}

	/**
	 * 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 )
	{
		$info  = self::get_asset_info();
		$types = $type ? array( $type ) : array( 'css', 'css_partial', 'js', 'js_partial' );

		foreach ( $types as $type ) {

			if ( isset( $info[ $type ] ) && file_exists( $info[ $type ] ) ) {
				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 ) {

			$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 ( file_exists( $path ) ) {
					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()
	{
		$cache_dir 	= self::get_cache_dir();
		$css 		= glob( $cache_dir['path'] . '*.css' );
		$js	 		= glob( $cache_dir['path'] . '*.js' );

		if ( is_array( $css ) ) {
			array_map( 'unlink', $css );
		}
		if ( is_array( $js ) ) {
			array_map( '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 = 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;
		}
		else {
			$data = self::get_layout_data( $status );
			$node = isset( $data[ $node_id ] ) ? $data[ $node_id ] : null;
		}

		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;
		}
		// Return nodes of a certain type.
		else {

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

				if ( $node->type == $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 );
		$status				= $template_post_id && ! self::is_post_node_template() ? 'published' : $status;
		$data  				= self::get_layout_data( $status, $template_post_id );
		$nodes 				= array();

		foreach ( $data as $node_id => $node ) {
			if ( $node->parent == $parent->node || ( $template_post_id && $parent->template_node_id == $node->parent ) ) {
				$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()
	{
		$nodes = array(
			'rows'	  => array(),
			'groups'  => array(),
			'columns' => array(),
			'modules' => array(),
		);

		if ( self::is_post_user_template( 'module' ) ) {
			$nodes['modules'] = self::get_all_modules();
		}
		else {
			$rows = self::get_nodes( 'row' );

			foreach ( $rows as $row ) {

				$nodes['rows'][ $row->node ] = $row;
				$groups = self::get_nodes( 'column-group', $row );

				foreach ( $groups as $group ) {

					$nodes['groups'][ $group->node ] = $group;
					$cols = self::get_nodes( 'column', $group );

					foreach ( $cols as $col ) {

						$nodes['columns'][ $col->node ] = $col;
						$col_children = self::get_nodes( null, $col );

						foreach ( $col_children as $col_child ) {

							if ( 'module' == $col_child->type ) {

								$module = self::get_module( $col_child );

								if ( $module ) {
									$nodes['modules'][ $col_child->node ] = $module;
								}
							}
							else if ( 'column-group' == $col_child->type ) {

								$nodes['groups'][ $col_child->node ] = $col_child;
								$group_cols = self::get_nodes( 'column', $col_child );

								foreach ( $group_cols as $group_col ) {

									$nodes['columns'][ $group_col->node ] = $group_col;
									$modules = self::get_modules( $group_col );

									foreach ( $modules as $module ) {
										$nodes['modules'][ $module->node ] = $module;
									}
								}
							}
						}
					}
				}
			}
		}

		return $nodes;
	}

	/**
	 * Returns node settings that are merged with the
	 * default or preview settings.
	 *
	 * @since 1.0
	 * @param object $node A node object.
	 * @return object
	 */
	static public function get_node_settings($node)
	{
		$post_data = self::get_post_data();

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

		// Get either the preview settings or saved node settings merged with the defaults.
		if(isset($post_data['node_preview']) && isset($post_data['node_id']) && $post_data['node_id'] == $node->node) {

			if(!isset($post_data['node_preview_processed_settings'])) {
				$settings = $post_data['node_preview'];
				$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 {
			$defaults = self::get_node_defaults($node);
			$settings = (object)array_merge((array)$defaults, (array)$node->settings);

			if ( 'module' == $node->type ) {
				$settings = self::merge_nested_module_defaults( $node->settings->type, $settings );
			}
		}

		return apply_filters( 'fl_builder_node_settings', $settings, $node );
	}

	/**
	 * 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($node->type == 'row') {
			$new_settings = self::process_row_settings($node, $new_settings);
		}
		if($node->type == 'column') {
			$new_settings = self::process_col_settings($node, $new_settings);
		}
		if($node->type == 'module') {
			$new_settings = self::process_module_settings($node, $new_settings);
		}

		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($node->type == 'row') {
			$defaults = self::get_row_defaults();
		}
		else if($node->type == 'column') {
			$defaults = self::get_col_defaults();
		}
		else if($node->type == 'module') {
			$defaults = self::get_module_defaults($node->settings->type);
		}

		return $defaults;
	}

	/**
	 * 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 ] );

		// Reorder sibling nodes.
		$siblings = self::get_nodes( $node->type, $node->parent );
		$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 = null, &$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($node->type == 'module' && isset(self::$modules[$node->settings->type])) {
			$class = get_class(self::$modules[$node->settings->type]);
			$instance = new $class();
			$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 ? $node->type : 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.
		foreach($nodes as $node) {
			$data[$node->node]->position = $new_pos;
			$new_pos++;
		}

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

	/**
	 * 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		= 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->node;

		// 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.
		self::reorder_node($node_id, $position);
	}

	/**
	 * Adds a row to the current layout.
	 *
	 * @since 1.0
	 * @param string $cols The type of column layout to use.
	 * @param int $position The position of the new row.
	 * @return object The new row object.
	 */
	static public function add_row($cols = '1-col', $position = false)
	{
		$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($position !== false) {
			self::reorder_node($row_node_id, $position);
		}

		// Add a column group.
		self::add_col_group($row_node_id, $cols, 0);

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

	/**
	 * Copys a row and adds it to the current layout.
	 *
	 * @since 1.0
	 * @param string $node_id Node ID of the row to copy.
	 * @return void
	 */
	static public function copy_row( $node_id = null )
	{
		$layout_data	= self::get_layout_data();
		$row			= self::get_node( $node_id );
		$new_row_id		= self::generate_node_id();
		$col_groups		= self::get_nodes( 'column-group', $row );
		$new_nodes		= 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 );
		}

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

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

			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 ) {

					$new_nodes[ $node->node ] = clone $node;

					if ( 'module' == $node->type ) {
						$new_nodes[ $node->node ]->settings = self::clone_module_settings( $node->settings );
					}
					else if ( 'column-group' == $node->type ) {

						$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 );
							}
						}
					}
				}
			}
		}

		// 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 ) {
			if ( $child->type == 'column-group' ) {
				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;
				}
			}
			if ( 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 );
			}
		}

		// 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()
	{
		return self::get_settings_form_defaults( 'row' );
	}

	/**
	 * Returns an array of spacing placeholders for row
	 * margins and padding.
	 *
	 * @since 1.9
	 * @return array
	 */
	static public function get_row_spacing_placeholders()
	{
		$settings     = FLBuilderModel::get_global_settings();
		$placeholders = array();

		// Default.
		$placeholders['row_margins'] = $settings->row_margins;
		$placeholders['row_padding'] = $settings->row_padding;

		// Medium.
		$placeholders['row_margins_medium'] = ( '' != $settings->row_margins_medium ) ? $settings->row_margins_medium : $settings->row_margins;
		$placeholders['row_padding_medium'] = ( '' != $settings->row_padding_medium ) ? $settings->row_padding_medium : $settings->row_padding;

		// Responsive row margins.
		if ( '' != $settings->row_margins_responsive ) {
			$placeholders['row_margins_responsive'] = $settings->row_margins_responsive;
		}
		else if ( $settings->auto_spacing ) {
			$placeholders['row_margins_responsive'] = 0;
		}
		else {
			$placeholders['row_margins_responsive'] = $placeholders['row_margins_medium'];
		}

		// Responsive row padding.
		if ( '' != $settings->row_padding_responsive ) {
			$placeholders['row_padding_tb_responsive'] = $settings->row_padding_responsive;
			$placeholders['row_padding_lr_responsive'] = $settings->row_padding_responsive;
		}
		else if ( $settings->auto_spacing ) {
			$placeholders['row_padding_tb_responsive'] = $placeholders['row_padding_medium'];
			$placeholders['row_padding_lr_responsive'] = 0;
		}
		else {
			$placeholders['row_padding_tb_responsive'] = $placeholders['row_padding_medium'];
			$placeholders['row_padding_lr_responsive'] = $placeholders['row_padding_medium'];
		}

		return $placeholders;
	}

	/**
	 * 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 ( $new_settings->bg_type == 'video' ) {

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

			if ( $new_settings->bg_video_source == 'wordpress' ) {
				// 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;
				}

				// 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;
				}
			}
		}

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

			// 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 ( $row->settings->bg_type == 'video' ) {

			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;
			}
		}

		// Background Slideshow
		else if ( $row->settings->bg_type == 'slideshow' && 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();
		}

		// 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();
	}

	/**
	 * 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.
	 * @return object The new column group object.
	 */
	static public function add_col_group($node_id = null, $cols = '1-col', $position = false)
	{
		$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;
				}
			}
		}
		// Add an existing column.
		else {

			$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($position !== false) {
			self::reorder_node($group_node_id, $position);
		}

		// 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.
		if ( isset( $post_data['node_preview'] ) ) {
			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);
		}

		// Resize the remaining columns.
		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, 2);
			}

			// 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;

		// Don't resize if only one column or width isn't a number.
		if($num_cols == 1 || !is_numeric($new_width)) {
			return $col->settings->size;
		}

		// Find the sibling column to absorb this resize.
		if($pos === 0) {
			$sibling = $cols[1];
		}
		else if($pos == $num_cols - 1) {
			$sibling = $cols[$num_cols - 2];
		}
		else {
			$sibling = $cols[$pos + 1];
		}

		// 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, 2);

		// 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 void
	 */
	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 );
	}

	/**
	 * 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 ( '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 ), 2 );

		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.
	 * @return object
	 */
	static public function add_cols( $col_id, $insert = 'before', $type = '1-col', $nested = false )
	{
		$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, 2 );
		}

		// 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++;
				}
			}
			else if ( $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 );

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

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

	/**
	 * Loads the classes for core builder modules.
	 *
	 * @since 1.0
	 * @return void
	 */
	static public function load_modules()
	{
		$path			= FL_BUILDER_DIR . 'modules/';
		$dir			= dir($path);
		$module_path	= '';

		while(false !== ($entry = $dir->read())) {

			if(!is_dir($path . $entry) || $entry == '.' || $entry == '..') {
				continue;
			}

			// Paths to check.
			$module_path	= $entry . '/' . $entry . '.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;
			}

			// Check for the module class in a parent theme.
			else if(file_exists($theme_path)) {
				require_once $theme_path;
			}

			// Check for the module class in the builder directory.
			else if(file_exists($builder_path)) {
				require_once $builder_path;
			}
		}
	}

	/**
	 * 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)) {

			// 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 ] ) ) {
				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;
			}

			// Filter the enabled flag.
			$instance->enabled = apply_filters( 'fl_builder_register_module', $instance->enabled, $instance );

			// Save the instance in the modules array.
			self::$modules[$instance->slug] = $instance;

			// Add the form to the instance.
			self::$modules[$instance->slug]->form = apply_filters( 'fl_builder_register_settings_form', $form, $instance->slug );
			self::$modules[$instance->slug]->form['advanced'] = self::$settings_forms['module_advanced'];
		}
	}

	/**
	 * 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 ] ) ) {
			_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;
		}

		$instance                = new stdClass;
		$instance->alias         = $alias;
		$instance->slug          = isset( $config['module'] ) ? $config['module'] : null;
		$instance->name          = isset( $config['name'] ) ? $config['name'] : $slug;
		$instance->description   = isset( $config['description'] ) ? $config['description'] : '';
		$instance->category      = isset( $config['category'] ) ? $config['category'] : null;
		$instance->settings      = isset( $config['settings'] ) ? $config['settings'] : array();
		$instance->enabled       = isset( $config['enabled'] ) ? $config['enabled'] : true;

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

	/**
	 * 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;
	}

	/**
	 * 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 all modules that are enabled.
	 *
	 * @since 1.0
	 * @return array
	 */
	static public function get_enabled_modules()
	{
		$default	= array_keys( self::$modules );
		$default[]	= 'all';
		$setting 	= self::get_admin_settings_option( '_fl_builder_enabled_modules', true );
		$setting    = ( ! $setting || in_array( 'all', $setting ) ) ? $default : $setting;

		foreach ( self::$modules as $module_slug => $module ) {
			if ( ! $module->enabled && in_array( $module_slug, $setting ) ) {
				$key = array_search( $module_slug, $setting );
				unset( $setting[ $key ] );
			}
		}

		return apply_filters( 'fl_builder_enabled_modules', $setting );
	}

	/**
	 * 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		 = array();

		// Add any predefined custom categories.
		foreach ( apply_filters( 'fl_builder_module_categories', array() ) as $custom_category ) {
			$categories[ $custom_category ] = array();
		}

		// Get the core category keys.
		$basic_key		 = __('Basic Modules', 'fl-builder');
		$advanced_key	 = __('Advanced Modules', 'fl-builder');
		$other_key		 = __('Other Modules', 'fl-builder');
		$widgets_key	 = __('WordPress Widgets', 'fl-builder');

		// Build the default category arrays.
		$categories[ $basic_key ] = array();
		$categories[ $advanced_key ] = array();
		$categories[ $other_key ] = array();

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

			if ( ! $module->enabled ) {
				continue;
			}
			else if(!in_array($module->slug, $enabled_modules) && !$show_disabled) {
				continue;
			}
			else if($module->slug == 'widget') {
				$widgets = self::get_wp_widgets();
			}
			else if(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;
	}

	/**
	 * Returns the slug for a module category.
	 *
	 * @since 1.0
	 * @param string $name The category name.
	 * @return string
	 */
	static public function get_module_category_slug( $name )
	{
		// Get the core category keys.
		$basic_key		 = __('Basic Modules', 'fl-builder');
		$advanced_key	 = __('Advanced Modules', 'fl-builder');
		$other_key		 = __('Other Modules', 'fl-builder');
		$widgets_key	 = __('WordPress Widgets', 'fl-builder');

		if ( $name == $basic_key ) {
			return 'basic';
		}
		if ( $name == $advanced_key ) {
			return 'advanced';
		}
		if ( $name == $other_key ) {
			return 'other';
		}
		if ( $name == $widgets_key ) {
			return 'widgets';
		}

		return sanitize_html_class( $name );
	}

	/**
	 * 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( 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;

			if ( isset( $module->template_id ) ) {
				$instance->template_id		= $module->template_id;
				$instance->template_node_id	= $module->template_node_id;
			}
			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]->form		= self::$modules[$module->settings->type]->form;

				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 array $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 = null, $settings = array(), $parent_id = null, $position = false )
	{
		$data				= self::get_layout_data();
		$parent 			= self::get_node( $parent_id );
		$module_node_id		= self::generate_node_id();
		$settings->type		= $type;

		// 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;
		}

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

		// Position the module.
		if($position !== false) {
			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 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( $parent_id = null, $position = null )
	{
		$parent = ! $parent_id ? null : self::get_node( $parent_id );

		// Add a new row if we don't have a parent.
		if ( ! $parent ) {
			$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;
		}

		// Add a new column group if the parent is a row.
		else if ( $parent->type == 'row' ) {
			$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;
		}

		// Add a new column if the parent is a column group.
		else if ( $parent->type == 'column-group' ) {
			$parent	    = self::add_col( $parent->node, $position );
			$parent_id	= $parent->node;
		}

		return $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();

		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.
	 */
	static public function add_default_module($parent_id = null, $type = null, $position = null, $defaults = null)
	{
		$parent			= $parent_id == 0 ? null : self::get_node($parent_id);
		$settings		= self::get_module_defaults($type);
		$module_node_id = self::generate_node_id();

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

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

		// 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							  = 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;

		// 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);
		}

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

	/**
	 * Make a copy of a module.
	 *
	 * @since 1.0
	 * @param string $node_id Node ID of the module to copy.
	 * @return object The new module object.
	 */
	static public function copy_module( $node_id = null )
	{
		$module	= self::get_module( $node_id );

		return self::add_module( $module->settings->type, $module->settings, $module->parent, $module->position + 1 );
	}

	/**
	 * 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;

		// Run node delete to clear any cache.
		$instance->settings = $module->settings;
		$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.
	 *
	 * @since 1.0
	 * @param string $type The type of module.
	 * @return object
	 */
	static public function get_module_defaults($type)
	{
		$defaults = new StdClass();

		if(isset(self::$modules[$type]->form)) {
			$defaults = self::get_settings_form_defaults( $type );
			$defaults->type = $type;
		}

		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 )
	{
		// Make sure the module form exists.
		if ( isset( self::$modules[ $type ] ) ) {

			// Get the fields.
			$fields = self::get_settings_form_fields( self::$modules[ $type ]->form );

			// 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 );
					}
				}
				else if ( ! empty( $settings->{ $key } ) ) {
					$settings->{ $key } = ( object )array_merge( ( array )$nested_defaults, ( array )$settings->{ $key } );
				}
				else {
					$settings->{ $key } = ( object )$nested_defaults;
				}
			}
		}

		return $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();

		// These are known widgets that won't work in the builder.
		$exclude = array(
			'WP_Widget_Media_Audio',
			'WP_Widget_Media_Image',
			'WP_Widget_Media_Video',
			'WP_Widget_Text',
		);

		foreach ( $wp_widget_factory->widgets as $class => $widget ) {
			if ( in_array( $class, $exclude ) ) {
				continue;
			}
			$widget->class = get_class( $widget );
			$widgets[ $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;
	}

	/**
	 * 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)
	{
		self::$settings_forms[$id] = apply_filters( 'fl_builder_register_settings_form', $form, $id );
	}

	/**
	 * 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 $form The form data array.
	 * @return array
	 */
	static public function get_settings_form_fields($form)
	{
		$fields = array();

		foreach ( $form as $tab ) {
			if ( isset( $tab['sections'] ) ) {
				foreach ( $tab['sections'] as $section ) {
					if ( isset( $section['fields'] ) ) {
						foreach ( $section['fields'] as $name => $field ) {
							$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'];
		}
		// If it's not a registered form, it must be a module form.
		else if ( isset( self::$modules[ $type ] ) ) {
			$form_type = $type . '-module';
			$tabs = self::$modules[ $type ]->form;
		}
		// The form can't be found.
		else {
			return $defaults;
		}

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

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

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

			if($is_multiple && $supports_multiple) {
				$defaults->$name = array($default);
			}
			else if ( $responsive ) {

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

					$responsive_name = $name . ( 'default' == $device ? '' : '_' . $device );

					if ( is_array( $responsive ) && isset( $responsive['default'] ) && isset( $responsive['default'][ $device ] ) ) {
						$defaults->{ $responsive_name } = $responsive['default'][ $device ];
					}
					else if( 'default' == $device ) {
						$defaults->$name = $default;
					}
					else {
						$defaults->{ $responsive_name } = '';
					}
				}
			}
			else {
				$defaults->$name = $default;
			}
		}

		// Cache the defaults.
		self::$settings_form_defaults[ $type ] = apply_filters( 'fl_builder_settings_form_defaults', $defaults, $form_type );

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

	/**
	 * 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);
		$new_settings		= (object)array_merge((array)$node->settings, (array)$settings);
		$template_post_id 	= self::is_node_global( $node );

		// Process the settings.
		$new_settings = self::process_node_settings($node, $new_settings);

		// 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 && ! self::is_post_node_template() ) {

			// 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 new layout.
		return FLBuilderAJAXLayout::render();
	}

	/**
	 * 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 );
			}
		}
		else if ( is_object( $data ) ) {
			foreach ( $data as $key => $val ) {
				$data->$key = self::slash_settings( $val );
			}
		}
		else if ( 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
	 * @return object
	 */
	static public function get_global_settings()
	{
		if ( null === self::$global_settings ) {
			$settings = get_option('_fl_builder_settings');
			$defaults = self::get_settings_form_defaults( 'global' );

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

			// Merge in defaults and cache settings
			self::$global_settings = (object) array_merge((array) $defaults, (array) $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())
	{
		$old_settings = self::get_global_settings();
		$new_settings = (object)array_merge((array)$old_settings, (array)$settings);

		self::delete_asset_cache_for_all_posts();
		self::$global_settings = null;

		update_option('_fl_builder_settings', $settings);

		return self::get_global_settings();
	}

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

		$post_id	  = self::get_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',
			'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) {

			$sql = "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) ";

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

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

				$sql_select[] = "SELECT {$new_post_id}, '{$meta_key}', '{$meta_value}'";
			}

			$sql .= implode(" UNION ALL ", $sql_select);
			// @codingStandardsIgnoreStart
			$wpdb->query($sql);
			// @codingStandardsIgnoreEnd
		}

		// 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 );
		if ( $template_id ) {
			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)
	{
		$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);
			}
		}
	}

	/**
	 * 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 published data?
		if($status == 'published') {
			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);
				self::$published_layout_data[$post_id] = self::clean_layout_data( $data );
			}
		}
		// Get draft data?
		else if($status == 'draft') {
			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);
				self::$draft_layout_data[$post_id] = self::clean_layout_data( $data );
			}
		}

		// 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) {
			$data[$node_id] = clone $node;
		}

		// Return the 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 );
		$data		= self::slash_settings( self::clean_layout_data( $data ) );

		// 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($status == 'published') {
			self::$published_layout_data[$post_id] = $data;
		}
		else if($status == 'draft') {
			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;
					}
					else {
						$cleaned[ $node->node ] = $node;
					}
				}
			}
		}

		return $cleaned;
	}

	/**
	 * 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 )
	{
		$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;
			}
			else if ( 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);

		// Fire the before action.
		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;
			}
			else if( $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
		));

		// Fire the after action.
		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 );
		}
	}

	/**
	 * 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);

		$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;
		}
		else if ( '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;
	}

	/**
	 * 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' );

		// Get the layout data and settings to copy.
		$data 		= self::get_layout_data();
		$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($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.
		do_action( 'fl_builder_after_save_user_template', $post_id );
	}

	/**
	 * 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' )
	{
		$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',
			'tax_query' => array(
				array(
					'taxonomy' => 'fl-builder-template-type',
					'field' => 'slug',
					'terms' => $type
				)
			)
		) );

		$templates = array();

		// Loop through templates posts and build the templates array.
		foreach( $posts as $post ) {

			if ( has_post_thumbnail( $post->ID ) ) {
				$image_data = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'medium' );
				$image = $image_data[0];
			}
			else {
				$image = FL_BUILDER_URL . 'img/templates/blank.jpg';
			}

			$templates[] = array(
				'id' 		=> $post->ID,
				'name'  	=> $post->post_title,
				'image' 	=> $image,
				'type'      => 'user'
			);
		}

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

			$cats = wp_get_post_terms( $template['id'], 'fl-builder-template-category' );

			if ( 0 === count( $cats ) || is_wp_error( $cats ) ) {
				$categorized['uncategorized']['templates'][] = $template;
			}
			else {

				foreach ( $cats as $cat ) {

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

					$categorized[ $cat->slug ]['templates'][] = $template;
				}
			}
		}

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

		// sort the categories.
		asort( $categorized );

		return array(
			'templates'  	=> $templates,
			'categorized' 	=> $categorized
		);
	}

	/**
	 * 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 ( 'fl-builder-template' != $post->post_type ) {
			return '';
		}
		else {

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

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

			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_delete_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 void
	 */
	static public function apply_user_template($template = null, $append = false)
	{
		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($template != 'blank') {

				// 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);

				// 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($node->type == 'row') {
							$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 array(
					'layout_css' => $settings->css
				);
			}
		}
	}

	/**
	 * 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.
	 * @return bool
	 */
	static public function is_post_node_template( $post_id = false )
	{
		$post_id = $post_id ? $post_id : self::get_post_id();
		$post    = get_post( $post_id );

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

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

			if ( in_array( $saved_type, array( 'row', '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 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;
		}

		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 ( self::is_builder_active() && $wp_the_query->post->ID == self::get_post_id() ) {
			return $is_visible;
		}

		if ( isset( $node->settings->visibility_display ) && ('' != $node->settings->visibility_display) ) {

			// For logged out users
			if ( $node->settings->visibility_display == 'logged_out' && ! is_user_logged_in() ) {
				$is_visible = true;
			}
			// For logged in users
			else if ( $node->settings->visibility_display == 'logged_in' && 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;
					}
				}
			}
			// Never
			else if ( $node->settings->visibility_display == 0 ) {
				$is_visible = false;
			} else {
				$is_visible = false;
			}

		}

		return apply_filters( 'fl_builder_is_node_visible', $is_visible, $node );
	}

	/**
	 * 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() )
	{
		foreach ( $nodes as $node ) {
			if ( $type == $node->type ) {
				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;
			}

			self::$node_template_post_ids[ $template_id ] = $posts[ 0 ]->ID;

			return $posts[ 0 ]->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 ) ) {

			$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'
					)
				),
				'post_type'	   => 'any',
				'post_status'  => 'any',
				'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;

		// 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'
		) );

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

		// Reset the root node's position.
		$root_node->position = 0;

		// 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 );

		// Add the template ID and template node ID for global templates.
		if ( $settings['global'] ) {

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

				$nodes[ $node_id ]->template_id = $template_id;
				$nodes[ $node_id ]->template_node_id = $node_id;

				if ( $node_id == $root_node->node ) {
					$nodes[ $node_id ]->template_root_node = true;
				}
				else if ( isset( $nodes[ $node_id ]->template_root_node ) ) {
					unset( $nodes[ $node_id ]->template_root_node );
				}
			}
		}
		// We need to remove the template ID and template node ID for standard templates.
		else {

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

				if ( isset( $nodes[ $node_id ]->template_id ) ) {
					unset( $nodes[ $node_id ]->template_id );
				}
				if ( isset( $nodes[ $node_id ]->template_node_id ) ) {
					unset( $nodes[ $node_id ]->template_node_id );
				}
				if ( 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', $settings['global'] );

		// Delete the existing node and apply the template for global templates.
		if ( $settings['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' 	=> $settings['global'] ? true : false,
			'link'		=> add_query_arg( 'fl_builder', '', get_permalink( $post_id ) ),
			'name'		=> $settings['name'],
			'type'		=> $root_node->type,
			'layout'	=> $settings['global'] ? FLBuilderAJAXLayout::render( $root_node->node, $template_node_id ) : null
		);
	}

	/**
	 * 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;
		}

		$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_delete_post( $template_post_id, true );
	}

	/**
	 * 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 ) {

				// Generate new node ids for the template data.
				$new_data = self::generate_new_node_ids( $template_data );

				// Get the root node from the template data.
				$root_node = self::get_node_template_root( $node->type, $new_data );

				// Remove the root node from the template data since it's already in the layout.
				unset( $new_data[ $root_node->node ] );

				// Update the settings for the root node in this layout.
				$layout_data[ $node_id ]->settings = $root_node->settings;

				// 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->node;
					}
				}

				// 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 ) {

			// Remove template info from the layout data.
			foreach ( $layout_data as $node_id => $node ) {
				unset( $layout_data[ $node_id ]->template_id );
				unset( $layout_data[ $node_id ]->template_post_id );
				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 sibiling nodes to update their position.
				foreach ( $layout_data as $i => $n ) {
					if ( $n->parent == $node->parent ) {
						$siblings[ $i ] = $n;
					}
				}

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

				// Update sibiling 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				= $parent_id == 0 ? null : self::get_node( $parent_id );
		$template_post_id 	= self::get_node_template_post_id( $template_id );

		// Allow extensions to hook into applying a 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;
		}
		// Get the template data.
		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 );
		}

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

		// Get the root node from the template data.
		$root_node = self::get_node_template_root( $type, $template_data );

		// Add a new parent for module node templates if needed.
		if ( 'module' == $root_node->type && ( ! $parent || 'row' == $parent->type || 'column-group' == $parent->type ) ) {
			$parent_id = self::add_module_parent( $parent_id, $position );
			$position  = null;
		}

		// 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 ];
		}
		// Merge all template data and settings for standard templates.
		else {

			// Merge template data.
			foreach ( $template_data as $node_id => $node ) {
				unset( $template_data[ $node_id ]->template_id );
				unset( $template_data[ $node_id ]->template_post_id );
				unset( $template_data[ $node_id ]->template_root_node );
			}

			$layout_data = array_merge( $layout_data, $template_data );

			// Merge template settings.
			$layout_settings = self::merge_layout_settings( $layout_settings, $template_settings );
		}

		// 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 );
		}

		// 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 sting $path The directory path to the template data file.
	 * @return void
	 */
	static public function register_templates( $path = false )
	{
		if ( $path && file_exists( $path ) ) {
			self::$templates[] = $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/*' );

		// glob() will return false on error so cast as an array() just in case.
		foreach ( (array) $templates as $template ) {

			if ( 'templates.dat' == basename( $template ) ) {
				continue;
			}

			self::register_templates( $template );
		}
	}

	/**
	 * 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.
		$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;
		}

		// Apply a core template.
		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 void
	 */
	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');

		// 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);

			// 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($node->type == 'row') {
						$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();
	}

	/**
	 * 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 $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();
					}

					// Reserialize the node data as it's expensive to store in memory.
					foreach ( $template_data as $key => $template ) {
						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();

		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 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();
		$core_categories = array(
			'general' => __( 'General', 'fl-builder' ),
			'landing' => __( 'Landing Pages', 'fl-builder' ),
			'company' => __( 'Content Pages', 'fl-builder' )
		);

		// Build the the templates array.
		foreach( self::get_templates( $type ) as $key => $template ) {

			if ( 'module' == $type ) {

				$nodes = maybe_unserialize( $template->nodes );
				$node  = array_shift( $nodes );

				if ( ! isset( self::$modules[ $node->settings->type ] ) ) {
					continue;
				}
			}

			if ( strstr( $template->image, '://' ) || strstr( $template->image, ';base64,' ) ) {
				$image = $template->image;
			}
			else {
				$image = FL_BUILDER_URL . 'img/templates/' . ( empty( $template->image ) ? 'blank.jpg' : $template->image );
			}

			$template_data = array(
				'id'       => $key,
				'name'     => $template->name,
				'image'    => $image,
				'category' => isset( $template->category ) ? $template->category : $template->categories,
				'type'     => 'core'
			);

			$template_data = apply_filters( 'fl_builder_template_selector_data', $template_data, $template );

			$templates[] = $template_data;
		}

		// Build the categorized templates array.
		foreach( $templates as $template ) {

			if ( ! isset( $template['category'] ) ) {
				continue;
			}

			if ( is_array( $template['category'] ) ) {

				foreach ( $template['category'] as $cat_key => $cat_label ) {

					if ( ! isset( $categorized[ $cat_key ] ) ) {
						$categorized[ $cat_key ] = array(
							'name'		=> $cat_label,
							'templates'	=> array()
						);
					}

					$categorized[ $cat_key ]['templates'][] = $template;
				}
			}
			else {

				if ( ! isset( $categorized[ $template['category'] ] ) ) {
					$categorized[ $template['category'] ] = array(
						'name'		=> $core_categories[ $template['category'] ],
						'templates'	=> array()
					);
				}

				$categorized[ $template['category'] ]['templates'][] = $template;
			}
		}

		// Return both the templates and categorized templates array.
		return apply_filters( 'fl_builder_template_selector_data', array(
			'templates'  	=> $templates,
			'categorized' 	=> $categorized
		), $type );
	}

	/**
	 * Returns data needed for the template selector's category filter.
	 *
	 * @since 1.8
	 * @return array
	 */
	static public function get_template_selector_filter_data()
	{
		$templates = self::get_template_selector_data();
		$data      = array();

		foreach ( $templates['categorized'] as $slug => $category ) {
			$data[ $slug ] = $category['name'];
		}

		return apply_filters( 'fl_builder_template_selector_filter_data', $data );
	}

	/**
	 * 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 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' ) );
	}

	/**
	 * 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 update_option( '_fl_builder_color_presets', $presets );
	}

	/**
	 * 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 __( 'Page 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 FL_BUILDER_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 );

		return ! $value ? array( 'font-awesome', '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'					=> true,
			'video'					=> true,
			'video_embed'			=> '<iframe src="https://player.vimeo.com/video/124230072?autoplay=1" width="420" height="315" frameborder="0" 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();
	}

	/**
	 * 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;

		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 ] );
		}

		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 )
	{
		// Get the site-wide option if we're in the network admin.
		if ( is_network_admin() ) {
			$value = get_site_option( $key );
		}
		// Get the site-wide option if network overrides aren't allowed.
		else if ( ! $network_override && class_exists( 'FLBuilderMultisiteSettings' ) ) {
			$value = get_site_option( $key );
		}
		// Network overrides are allowed. Return the subsite option if it exists.
		else if ( class_exists( 'FLBuilderMultisiteSettings' ) ) {
			$value = get_option( $key );
			$value = false === $value ? get_site_option( $key ) : $value;
		}
		// This must be a single site install. Get the single site option.
		else {
			$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 )
	{
		// Update the site-wide option since we're in the network admin.
		if ( is_network_admin() ) {
			update_site_option( $key, $value );
		}
		// Delete the option if network overrides are allowed and the override checkbox isn't checked.
		else if ( $network_override && FLBuilderAdminSettings::multisite_support() && ! isset( $_POST['fl-override-ms'] ) ) {
			delete_option( $key );
		}
		// Update the option for single install or subsite.
		else {
			update_option( $key, $value );
		}
	}

	/**
	 * 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_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();
			$filesystem	 = FLBuilderUtils::get_filesystem();
			$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;
		}
	}

	/**
	 * @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' );
	}
}

FLBuilderModel::init();
