<?php
/**
* ShortcodeHelper class that holds information on allowed shortcodes, formating functions etc
*/

// Don't load directly
if( ! defined('ABSPATH') ) { exit; }

if( ! class_exists( 'ShortcodeHelper', false ) )
{
	class ShortcodeHelper
	{
		/**
		 *
		 * @var array
		 */
		static public $allowed_shortcodes = array();

		/**
		 *
		 * @var array
		 */
		static public $manually_allowed_shortcodes = array();

		/**
		 *
		 * @var array
		 */
		static public $manually_disallowed_shortcodes = array();

		/**
		 *
		 * @var array
		 */
		static public $nested_shortcodes = array();

		/**
		 *
		 * @var string
		 */
		static public $pattern = '';

		/**
		 *
		 * @var array
		 */
		static public $tree = array();

		/**
		 * tells us which index the currently rendered shortcode has
		 *
		 * @var int
		 */
		static public $shortcode_index = 0;

		/**
		 * Stores the post that had been base for the tree. In loops of posts we have to make sure we have the appropriate tree loaded for the post.
		 * Otherwise this can result in $meta['index'] undefined
		 *
		 * @since 4.5.5
		 * @var WP_Post|null
		 */
		static public $current_post_in_tree = null;

		/**
		 *
		 * @var int
		 */
		static public $direct_calls = 0;			//	adds direct calls to theme shortcodes (e.g. from codeblocks to update $shortcode_index correctly)

		/**
		 *
		 * @var boolean
		 */
		static public $is_direct_call = false;		//	set to true if shortcodes are eecuted inside elements and removed from final code to execute like in codeblocks (shortcode tree is incorrect in that case)


		/**
		 * Converts a shortcode string into an array. Allows nested shortcodes.
		 * Make sure that content is unslashed ( call wp_unslash() )
		 *
		 * @param string $content
		 * @param int $depth
		 * @return array
		 */
		static public function shortcode2array( $content, $depth = 1000 )
		{
			$pattern = empty( ShortcodeHelper::$pattern) ? ShortcodeHelper::build_pattern() : ShortcodeHelper::$pattern;
			$depth --;

			$matches = array();
			preg_match_all( "/$pattern/s", $content , $matches );

			$return = array();

			foreach( $matches[3] as $key => $match )
			{
				$return[ $key ]['shortcode'] = $matches[2][ $key ];
				$return[ $key ]['attr'] = shortcode_parse_atts( $match );
				$return[ $key ]['raw_content'] = $matches[5][ $key ];

				if( preg_match( "/$pattern/s", $matches[5][ $key ] ) && $depth )
				{
					$return[ $key ]['content'] = self::shortcode2array( $matches[5][ $key ], $depth );
				}
				else
				{
					$return[ $key ]['content'] = $matches[5][ $key ];
				}
			}

			return $return;
		}

		/**
		 * Set the allowed shortcodes
		 *
		 * @param array $params
		 * @param boolean $remove
		 **/
		static public function allowed_shortcodes( $params, $remove = false )
		{
			if( ! $remove )
			{
				self::$manually_allowed_shortcodes = $params;
				if( ! in_array( 'av_textblock', self::$manually_allowed_shortcodes ) )
				{
					self::$manually_allowed_shortcodes[] = 'av_textblock';
				}
			}
			else
			{
				self::$manually_disallowed_shortcodes = $params;
			}
		}

		/**
		 * Checks if the current element is nested within column or section
		 *
		 * @return boolean
		 */
		static public function is_top_level()
		{
			global $avia_config;

			if( ! isset( $avia_config['conditionals']['is_builder'] ) )
			{
				return false;
			}

			if( isset( $avia_config['current_column'] ) || isset( $avia_config['layout_container'] ) || isset( $avia_config['icongrid_container'] ) )
			{
				return false;
			}

			return true;
		}


		/**
		 * creates the shortcode pattern that only matches Avia Builder Shortcodes
		 *
		 * @param array|false $predefined_tags
		 * @return string
		 **/
		static public function build_pattern( $predefined_tags = false )
		{
			global $shortcode_tags;

			//save the 'real' shortcode array
			$old_sc = $shortcode_tags;

			//if we got allowed shortcodes build the pattern. nested shortcodes are also considered but within a separate array
			if( ! empty( ShortcodeHelper::$allowed_shortcodes ) )
			{
				$shortcode_tags = array_flip( array_merge( ShortcodeHelper::$allowed_shortcodes, ShortcodeHelper::$nested_shortcodes ) );
			}

			// filter out all elements that are not in the predefined tags array. this is necessary for nested shortcode modal to work properly
			if( is_array( $predefined_tags ) )
			{
				$predefined_tags = array_flip( $predefined_tags );
				$shortcode_tags = shortcode_atts( $predefined_tags, $shortcode_tags );
			}

			//create the pattern and store it
			ShortcodeHelper::$pattern = get_shortcode_regex();

			//restore original shortcode tags
			$shortcode_tags = $old_sc;

			return ShortcodeHelper::$pattern;
		}

		/**
		 * Create a fake pattern on the fly that makes us able to check a post for shortcodes upfront,
		 * just so we know if we need to load any special resources. Can also returns the complete
		 * shortcode tags (open or close, not the content) to check and balance the shortcodes.
		 *
		 * With 4.2.1 this function also returns the escaped shortcodes.
		 *
		 * @param boolean $nested
		 * @param array|false $shortcode_tags
		 * @param string $return					'fake' | 'tags' (added with 4.3) | 'complete'  (added with 4.1.2)
		 * @return string
		 **/
		static public function get_fake_pattern( $nested = false, $shortcode_tags = false, $return = 'fake' )
		{
			if( ! is_array( $shortcode_tags ) )
			{
				if( ! empty( ShortcodeHelper::$allowed_shortcodes ) )
				{
					//usually we dont want the nested fake shortcodes to be included in the count
					if( $nested )
					{
						$shortcode_tags = array_merge( ShortcodeHelper::$allowed_shortcodes, ShortcodeHelper::$nested_shortcodes );
					}
					else
					{
						$shortcode_tags = array_merge( ShortcodeHelper::$allowed_shortcodes );
					}
				}
			}

			if( 'fake' == $return )
			{
				//	match all shortcodes and escaped shortcodes start and end tags:  [xxx  [[xxx  [/xxx]  [/xxx]]
				$pattern = "\[{1,2}" . implode( '[\s|\]]|\[{1,2}', $shortcode_tags ) . "[\s|\]]|\[\/" . implode( '\]{1,2}|\[\/', $shortcode_tags ) . "\]{1,2}";
			}
			else
			{
				$arr_patt = array();

				foreach( $shortcode_tags as $key => $shortcode_tag )
				{
					/**
					 *	captures unclosed tags like "[fooo ...  [ " and  [/foo .... [  - must be placed first otherwise will be included in standard match
					 *
					 * (?=\[) includes everything up to next [ (but not including [)
					 */
					if( 'complete' == $return )
					{
						$arr_patt[] = '\[{1,2}[\/]?' . $shortcode_tag . '[^\]]*?(?=\[)';
					}

					//	captures complete opening and closing shortcode tags
					$arr_patt[] = '\[{1,2}[\/]?' . $shortcode_tag . '[^\]]*\]{1,2}';

				}

				$pattern = implode( '|', $arr_patt );
			}

			return $pattern;
		}

		/**
		 * Builds a shortcode tree out of an array that was extracted from the content with get_fake_pattern and a call to
		 * preg_match_all( ...., PREG_OFFSET_CAPTURE ).
		 *
		 * Also removes shortcodes inside escaped shortcodes and escaped shortcodes as WP ignores these.
		 *
		 * Ensure that the HTML shortcode structure in page content is balanced with all registered shortcodes before calling this function,
		 * otherwise this might lead to a broken layout because WP might work with a different shortcode structure
		 * https://codex.wordpress.org/Shortcode_API#Unclosed_Shortcodes
		 *
		 * ( also see ShortcodeHelper::balance_shortcode() )
		 *
		 * @param string $content
		 * @return array
		 */
		static public function &build_shortcode_tree( $content )
		{

			//	see avia_sc_postcontent (currently experimental only)
//          if( is_array( $matches[0] ) )
//			{
//				$matches = $matches[0];
//			}

			$tree = array();
			$wp_result = array();

			$parser_state = Avia_Builder()->get_posts_shortcode_parser_state();

			if( in_array( $parser_state, array( 'disabled' ) ) || ! in_array( Avia_Builder()->get_shortcode_parser()->get_shortode_state(), array( 'valid' ) ) )
			{
				/**
				 * This is for a fallback situation when user does not want to or cannot use the autorepair function because it breaks.
				 * We use the old style from version <= 4.2
				 *
				 * Extract all shortcodes from the post array and store them so we know what we are dealing with when the user opens a page.
				 * Usesfull for special elements that we might need to render outside of the default loop like fullscreen slideshows
				 */

				$matches = array();

				preg_match_all( "/" . ShortcodeHelper::get_fake_pattern() . "/s", $content, $matches );
				if( is_array( $matches ) && is_array( $matches[0] ) && ( ! empty( $matches[0] ) ) )
				{
					$matches = $matches[0];
				}
				else
				{
					return $tree;
				}

				$matches = explode( ',', str_replace( ']', '', implode( ',', $matches ) ) );

				/**
				 * Close all elements that are not self closing to generate a valid xml string
				 */
				$current = 0;

				while ( $current < count( $matches ) )
				{
					$match = trim( $matches[ $current ] );

					if( strpos( $match, '/' ) !== false )
					{
						$current ++;
						continue;
					}

					$closing = trim(str_replace('[','[/',$match));

					$search = $current + 1;
					$auto_close = true;

					while ( $search < count( $matches ) )
					{
						$comp = trim( $matches[ $search ] );

						if( $match == $comp )
						{
							break;
						}

						if( $closing == $comp )
						{
							$auto_close = false;
							break;
						}

						$search ++;
					}

					if( $auto_close ) //if we got no closing tag add a temp one
					{
						array_splice( $matches, $current + 1, 0, array( $closing ) );
					}

					$current ++;
				}
				/**
				 * Reset to new style array
				 */
				foreach( $matches as $sc )
				{
					$wp_result[] = array( $sc );
				}
			}
			else
			{
				$parser = Avia_Builder()->get_shortcode_parser();

				/**
				 * This is the preferred way since 4.2.1
				 *
				 * To be on the safe side we use pure WP regex to extract the shortcodes.
				 * As content should have been balanced before calling this function we can rely on a
				 * valid shortcode structure
				 */
				$wp_result = $parser->get_wp_result( $content );

				/**
				 * Ensure we have a valid shortcode structure before calling this function !!!
				 * Now we scan for escaped shortcodes and add missing closing tags to self closing tags so we have a valid XML structure
				 * for easier building of the tree.
				 */
				$wp_result = $parser->balance_shortcode_array( $wp_result, 'forced' );

				/**
				 * Remove escaped shortcodes and extract only ALB shortcodes that have a valid callback
				 */
				$parser->remove_escape_shortcodes_in_array( $wp_result );
				$parser->filter_shortcodes_in_array( $wp_result, ShortcodeHelper::$allowed_shortcodes );
			}

			/*
			 * As we can rely on valid shortcode structure now we can safely use the truncated shortcode without attributes
			 */
			$sc = array();
			foreach ( $wp_result as $code )
			{
				$sc[] = $code[0];
			}

			$shortcodes = explode( ',', str_replace( array( ']', '[' ), '', implode( ',', $sc ) ) );

			$temp_index = 0;
			$tree       = array('content' => array());
			$pointers   = array(&$tree);

			foreach( $shortcodes as $index => $line )
			{
				if( ! empty( $line[0] ) )
				{
					$close = '/' === $line[0];
					$count = count($pointers);

					if( ! $close )
					{
						$pointers[ $count ] = array( 'tag' => trim( $line ), 'content' => array(), 'index' => $temp_index );
						$pointers[ $count - 1 ]['content'][] = &$pointers[ $count ];
						$temp_index ++ ;
						continue;
					}
					else
					{
						array_pop( $pointers );
					}
				}
			}

			$result = &$tree['content'];
			unset( $tree );

			return $result;
		}

		/**
		 *
		 * @param int $index
		 * @param int|false $sibling
		 * @param array|false $tree
		 * @return array|false
		 */
		static public function find_tree_item( $index, $sibling = false, $tree = false )
		{
			if( empty( self::$tree ) )
			{
				return false;
			}

			if( $tree === false )
			{
				$tree = self::$tree;
			}

			$return = array();

			foreach( $tree as $key => $t )
			{
				if( ! $return )
				{
					if( $t['index'] == $index )
					{
						if( $sibling !== false )
						{
							$return = isset( $tree[ $key + $sibling ] ) ? $tree[ $key + $sibling ] : false;
						}
						else
						{
							$return = $tree[ $key ];
						}

						return $return;
					}
					else if( ! empty( $tree[ $key ]['content'] ) )
					{
						$return = self::find_tree_item( $index, $sibling, $tree[ $key ]['content'] );
					}
				}
			}

			return $return;
		}

		/**
		 * Clean up the rendered text and ensure balanced shortcode tags (depending on shortcode parser setting).
		 * $text can be content of an ALB paga, a non ALB page, _aviaLayoutBuilderCleanData or ALB template
		 *
		 *		- Checks, that existing closing shortcode tags are applied to all opening shortcode tags ( nesting of same name shortcode is not supported )
		 *		- Removes all unnecessary html tags around shortcodes that are added by the editor
		 *
		 * @param string $text
		 * @param string $action			'content' | 'balance_only'
		 * @return string
		 */
		static public function clean_up_shortcode( $text, $action = 'balance_only' )
		{
/*
			self::build_pattern();
            $text_nodes = preg_split("/".self::$pattern."/s", $text);

            //usually removes all <p> tags that are not needed before shorttcode and in between them like </p><p>
            foreach($text_nodes as $node )
			{
	            if( strlen( trim( $node ) ) == 0 || strlen( trim( strip_tags($node) ) ) == 0)
	            {
	               $text = preg_replace("/(".preg_quote($node, '/')."(?!\[\/))/", '', $text);
	            }
	        }

*/

			/**
			 * Activates the shortcode parser depending on shortcode parser flag setting and generates the error output to DB
			 */
			$text = Avia_Builder()->get_shortcode_parser()->parse_shortcode( $text );


			/**
			 * On a non ALB page, _aviaLayoutBuilderCleanData and template we only need to add the missing shortcode tags and do not
			 * touch the structure of the page
			 */
			if( 'balance_only' == $action )
			{
				return $text;
			}

			/**
			 * On ALB page content:
			 *
			 * remove all <br/> tags that are not needed. those that follow immediately after a shortode or are located just before one
			 */
			$shortcode_tags = array_merge( ShortcodeHelper::$allowed_shortcodes, ShortcodeHelper::$nested_shortcodes );

			$tagregexp = join( '|', array_map( 'preg_quote', $shortcode_tags ) );

			$regex = "!(\s*?\<br.?/?>.?)*?(\[\/?($tagregexp).*?\])(\s*?\<br.?/?>.?)*!s";
			$text = preg_replace( $regex, '${2}', $text );

			return $text;
		}

		/**
		 * Removes wordpress autop and invalid nesting of p tags, as well as br tags
		 *
		 * @param string $content				html content by the wordpress editor
		 * @param boolean $do_shortcode
		 * @return string $content
		 */
		static public function avia_remove_autop( $content, $do_shortcode = false )
		{
			$shortcode_tags = array_merge( ShortcodeHelper::$allowed_shortcodes, ShortcodeHelper::$nested_shortcodes );
			$tagregexp = join( '|', array_map( 'preg_quote', $shortcode_tags ) );

			// opening tag
			$content = preg_replace( "/(<p>)?\[($tagregexp)(\s[^\]]+)?\](<\/p>|<br \/>)?/", "[$2$3]", $content );

			// closing tag
			$content = preg_replace( "/(<p>)?\[\/($tagregexp)](<\/p>|<br \/>)?/", "[/$2]", $content );


			if( $do_shortcode )
			{
				$content = do_shortcode( shortcode_unautop( $content ) );
			}

			$content = preg_replace( '#^<\/p>|^<br\s?\/?>|<p>$|<p>\s*(&nbsp;)?\s*<\/p>#', '', $content );

			return $content;
		}

		/**
		 * Applies wordpress autop filter
		 *
		 * @param string $content				html content by the wordpress editor
		 * @param boolean
		 * @return string $content
		 */
		static public function avia_apply_autop( $content, $do_shortcode = true )
		{
			$content = wpautop( $content );

			if( $do_shortcode )
			{
				$content = do_shortcode( shortcode_unautop( $content ) );
			}

			return $content;
		}

		/**
		 * Creates the shortcode based on variables that are passed
		 *
		 * @param string $name
		 * @param string|null $content
		 * @param array $args
		 * @return string
		 */
		static public function create_shortcode_by_array( $name, $content = null, $args = array() )
		{
			$sc = '[' . $name;

			if( is_array( $args ) )
			{
				foreach( $args as $key => $arg )
				{
					if( is_numeric( $key ) )
					{
						$sc .= ' ' . $arg;
					}
					else
					{
						//	if attributes array is coming from frontend we can have arrays (e.g. link)
						if( is_array( $arg ) )
						{
							$arg = implode( ',', $arg );
						}

						if( strpos( $arg, "'" ) === false && strpos( $arg, "&#039;" ) === false )
						{
							$sc .= ' ' . $key . "='" . $arg . "'";
						}
						else
						{
							$sc .= ' ' . $key . '="' . $arg . '"';
						}
					}
				}
			}

			$sc .= ']';

			if( ! is_null( $content ) )
			{
				//strip slashes and trim the content
				$content = "\n" . trim( stripslashes( $content ) ) . "\n";

				// $content = htmlentities( $content , ENT_QUOTES, get_bloginfo( 'charset' ) ); //entity-test: added htmlentities

				//if the content is empty without tabs and line breaks remove it completly
				if( trim( $content ) == '' )
				{
					$content = '';
				}

				$sc .= "{$content}[/{$name}]";
			}

			//  important: add 2 linebreaks otherwise WP will add a <br> tag and break frontend layout ( wpautop() ) !!!
			$sc .= "\n\n";

			return $sc;
		}

		/**
		 * Creates the complete shortcode string of an ALB element from an attribute array.
		 * Content can be an array of attributes or a string.
		 * Checks for self closing shortcode.
		 * If 'content' is an array, then nested shortcodes will be created for each entry, otherwise content is copied 1:1
		 *
		 * @since 4.8
		 * @param aviaShortcodeTemplate $shortcode
		 * @param array $attr
		 * @return string
		 */
		static public function create_shortcode_from_attributes_array( aviaShortcodeTemplate $shortcode, array $attr = array() )
		{
			$item_is_self_closing = $shortcode->has_modal_group_template() && $shortcode->is_nested_self_closing( $shortcode->config['shortcode_nested'][0] );

			$inner_content = '';

			if( $shortcode->has_modal_group_template() && isset( $attr['content'] ) )
			{
				if( ! is_array( $attr['content'] ) )
				{
					$inner_content = $attr['content'];
				}
				else
				{
					$inner_content = array();

					foreach( $attr['content'] as $item )
					{
						if( ! is_array( $item ) )
						{
							$inner_content[] = $item;
							continue;
						}

						$item_cont = '';
						if( isset( $item['content'] ) )
						{
							$item_cont = $item['content'];
							unset( $item['content'] );
						}

						if( $item_is_self_closing )
						{
							$item_cont = null;
						}

						$inner_content[] = ShortcodeHelper::create_shortcode_by_array( $shortcode->config['shortcode_nested'][0], $item_cont, $item );
					}

					$inner_content = implode( "\n", $inner_content );
				}
			}
			else if( isset( $attr['content'] ) )
			{
				$inner_content = $attr['content'];
			}

			if( $shortcode->is_self_closing() )
			{
				$inner_content = null;
			}

			unset( $attr['content'] );

			return ShortcodeHelper::create_shortcode_by_array( $shortcode->config['shortcode'], $inner_content, $attr );
		}
	}

}
