| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 | <?php/** * WordPress API for creating bbcode-like tags or what WordPress calls * "shortcodes". The tag and attribute parsing or regular expression code is * based on the Textpattern tag parser. * * A few examples are below: * * [shortcode /] * [shortcode foo="bar" baz="bing" /] * [shortcode foo="bar"]content[/shortcode] * * Shortcode tags support attributes and enclosed content, but does not entirely * support inline shortcodes in other shortcodes. You will have to call the * shortcode parser in your function to account for that. * * {@internal * Please be aware that the above note was made during the beta of WordPress 2.6 * and in the future may not be accurate. Please update the note when it is no * longer the case.}} * * To apply shortcode tags to content: * *     $out = do_shortcode( $content ); * * @link https://developer.wordpress.org/plugins/shortcodes/ * * @package WordPress * @subpackage Shortcodes * @since 2.5.0 *//** * Container for storing shortcode tags and their hook to call for the shortcode * * @since 2.5.0 * * @name $shortcode_tags * @var array * @global array $shortcode_tags */$shortcode_tags = array();/** * Adds a new shortcode. * * Care should be taken through prefixing or other means to ensure that the * shortcode tag being added is unique and will not conflict with other, * already-added shortcode tags. In the event of a duplicated tag, the tag * loaded last will take precedence. * * @since 2.5.0 * * @global array $shortcode_tags * * @param string   $tag      Shortcode tag to be searched in post content. * @param callable $callback The callback function to run when the shortcode is found. *                           Every shortcode callback is passed three parameters by default, *                           including an array of attributes (`$atts`), the shortcode content *                           or null if not set (`$content`), and finally the shortcode tag *                           itself (`$shortcode_tag`), in that order. */function add_shortcode( $tag, $callback ) {	global $shortcode_tags;	if ( '' == trim( $tag ) ) {		$message = __( 'Invalid shortcode name: Empty name given.' );		_doing_it_wrong( __FUNCTION__, $message, '4.4.0' );		return;	}	if ( 0 !== preg_match( '@[<>&/\[\]\x00-\x20=]@', $tag ) ) {		/* translators: 1: Shortcode name, 2: Space-separated list of reserved characters. */		$message = sprintf( __( 'Invalid shortcode name: %1$s. Do not use spaces or reserved characters: %2$s' ), $tag, '& / < > [ ] =' );		_doing_it_wrong( __FUNCTION__, $message, '4.4.0' );		return;	}	$shortcode_tags[ $tag ] = $callback;}/** * Removes hook for shortcode. * * @since 2.5.0 * * @global array $shortcode_tags * * @param string $tag Shortcode tag to remove hook for. */function remove_shortcode( $tag ) {	global $shortcode_tags;	unset( $shortcode_tags[ $tag ] );}/** * Clear all shortcodes. * * This function is simple, it clears all of the shortcode tags by replacing the * shortcodes global by a empty array. This is actually a very efficient method * for removing all shortcodes. * * @since 2.5.0 * * @global array $shortcode_tags */function remove_all_shortcodes() {	global $shortcode_tags;	$shortcode_tags = array();}/** * Whether a registered shortcode exists named $tag * * @since 3.6.0 * * @global array $shortcode_tags List of shortcode tags and their callback hooks. * * @param string $tag Shortcode tag to check. * @return bool Whether the given shortcode exists. */function shortcode_exists( $tag ) {	global $shortcode_tags;	return array_key_exists( $tag, $shortcode_tags );}/** * Whether the passed content contains the specified shortcode * * @since 3.6.0 * * @global array $shortcode_tags * * @param string $content Content to search for shortcodes. * @param string $tag     Shortcode tag to check. * @return bool Whether the passed content contains the given shortcode. */function has_shortcode( $content, $tag ) {	if ( false === strpos( $content, '[' ) ) {		return false;	}	if ( shortcode_exists( $tag ) ) {		preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER );		if ( empty( $matches ) ) {			return false;		}		foreach ( $matches as $shortcode ) {			if ( $tag === $shortcode[2] ) {				return true;			} elseif ( ! empty( $shortcode[5] ) && has_shortcode( $shortcode[5], $tag ) ) {				return true;			}		}	}	return false;}/** * Search content for shortcodes and filter shortcodes through their hooks. * * If there are no shortcode tags defined, then the content will be returned * without any filtering. This might cause issues when plugins are disabled but * the shortcode will still show up in the post or content. * * @since 2.5.0 * * @global array $shortcode_tags List of shortcode tags and their callback hooks. * * @param string $content Content to search for shortcodes. * @param bool $ignore_html When true, shortcodes inside HTML elements will be skipped. * @return string Content with shortcodes filtered out. */function do_shortcode( $content, $ignore_html = false ) {	global $shortcode_tags;	if ( false === strpos( $content, '[' ) ) {		return $content;	}	if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) {		return $content;	}	// Find all registered tag names in $content.	preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $content, $matches );	$tagnames = array_intersect( array_keys( $shortcode_tags ), $matches[1] );	if ( empty( $tagnames ) ) {		return $content;	}	$content = do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames );	$pattern = get_shortcode_regex( $tagnames );	$content = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $content );	// Always restore square braces so we don't break things like <!--[if IE ]>	$content = unescape_invalid_shortcodes( $content );	return $content;}/** * Retrieve the shortcode regular expression for searching. * * The regular expression combines the shortcode tags in the regular expression * in a regex class. * * The regular expression contains 6 different sub matches to help with parsing. * * 1 - An extra [ to allow for escaping shortcodes with double [[]] * 2 - The shortcode name * 3 - The shortcode argument list * 4 - The self closing / * 5 - The content of a shortcode when it wraps some content. * 6 - An extra ] to allow for escaping shortcodes with double [[]] * * @since 2.5.0 * @since 4.4.0 Added the `$tagnames` parameter. * * @global array $shortcode_tags * * @param array $tagnames Optional. List of shortcodes to find. Defaults to all registered shortcodes. * @return string The shortcode search regular expression */function get_shortcode_regex( $tagnames = null ) {	global $shortcode_tags;	if ( empty( $tagnames ) ) {		$tagnames = array_keys( $shortcode_tags );	}	$tagregexp = join( '|', array_map( 'preg_quote', $tagnames ) );	// WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag()	// Also, see shortcode_unautop() and shortcode.js.	// phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation	return		'\\['                                // Opening bracket		. '(\\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]		. "($tagregexp)"                     // 2: Shortcode name		. '(?![\\w-])'                       // Not followed by word character or hyphen		. '('                                // 3: Unroll the loop: Inside the opening shortcode tag		.     '[^\\]\\/]*'                   // Not a closing bracket or forward slash		.     '(?:'		.         '\\/(?!\\])'               // A forward slash not followed by a closing bracket		.         '[^\\]\\/]*'               // Not a closing bracket or forward slash		.     ')*?'		. ')'		. '(?:'		.     '(\\/)'                        // 4: Self closing tag ...		.     '\\]'                          // ... and closing bracket		. '|'		.     '\\]'                          // Closing bracket		.     '(?:'		.         '('                        // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags		.             '[^\\[]*+'             // Not an opening bracket		.             '(?:'		.                 '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag		.                 '[^\\[]*+'         // Not an opening bracket		.             ')*+'		.         ')'		.         '\\[\\/\\2\\]'             // Closing shortcode tag		.     ')?'		. ')'		. '(\\]?)';                          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]	// phpcs:enable}/** * Regular Expression callable for do_shortcode() for calling shortcode hook. * * @see get_shortcode_regex for details of the match array contents. * * @since 2.5.0 * @access private * * @global array $shortcode_tags * * @param array $m Regular expression match array * @return string|false False on failure. */function do_shortcode_tag( $m ) {	global $shortcode_tags;	// allow [[foo]] syntax for escaping a tag	if ( $m[1] == '[' && $m[6] == ']' ) {		return substr( $m[0], 1, -1 );	}	$tag  = $m[2];	$attr = shortcode_parse_atts( $m[3] );	if ( ! is_callable( $shortcode_tags[ $tag ] ) ) {		/* translators: %s: Shortcode tag. */		$message = sprintf( __( 'Attempting to parse a shortcode without a valid callback: %s' ), $tag );		_doing_it_wrong( __FUNCTION__, $message, '4.3.0' );		return $m[0];	}	/**	 * Filters whether to call a shortcode callback.	 *	 * Returning a non-false value from filter will short-circuit the	 * shortcode generation process, returning that value instead.	 *	 * @since 4.7.0	 *	 * @param false|string $return      Short-circuit return value. Either false or the value to replace the shortcode with.	 * @param string       $tag         Shortcode name.	 * @param array|string $attr        Shortcode attributes array or empty string.	 * @param array        $m           Regular expression match array.	 */	$return = apply_filters( 'pre_do_shortcode_tag', false, $tag, $attr, $m );	if ( false !== $return ) {		return $return;	}	$content = isset( $m[5] ) ? $m[5] : null;	$output = $m[1] . call_user_func( $shortcode_tags[ $tag ], $attr, $content, $tag ) . $m[6];	/**	 * Filters the output created by a shortcode callback.	 *	 * @since 4.7.0	 *	 * @param string       $output Shortcode output.	 * @param string       $tag    Shortcode name.	 * @param array|string $attr   Shortcode attributes array or empty string.	 * @param array        $m      Regular expression match array.	 */	return apply_filters( 'do_shortcode_tag', $output, $tag, $attr, $m );}/** * Search only inside HTML elements for shortcodes and process them. * * Any [ or ] characters remaining inside elements will be HTML encoded * to prevent interference with shortcodes that are outside the elements. * Assumes $content processed by KSES already.  Users with unfiltered_html * capability may get unexpected output if angle braces are nested in tags. * * @since 4.2.3 * * @param string $content Content to search for shortcodes * @param bool $ignore_html When true, all square braces inside elements will be encoded. * @param array $tagnames List of shortcodes to find. * @return string Content with shortcodes filtered out. */function do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames ) {	// Normalize entities in unfiltered HTML before adding placeholders.	$trans   = array(		'[' => '[',		']' => ']',	);	$content = strtr( $content, $trans );	$trans   = array(		'[' => '[',		']' => ']',	);	$pattern = get_shortcode_regex( $tagnames );	$textarr = wp_html_split( $content );	foreach ( $textarr as &$element ) {		if ( '' == $element || '<' !== $element[0] ) {			continue;		}		$noopen  = false === strpos( $element, '[' );		$noclose = false === strpos( $element, ']' );		if ( $noopen || $noclose ) {			// This element does not contain shortcodes.			if ( $noopen xor $noclose ) {				// Need to encode stray [ or ] chars.				$element = strtr( $element, $trans );			}			continue;		}		if ( $ignore_html || '<!--' === substr( $element, 0, 4 ) || '<![CDATA[' === substr( $element, 0, 9 ) ) {			// Encode all [ and ] chars.			$element = strtr( $element, $trans );			continue;		}		$attributes = wp_kses_attr_parse( $element );		if ( false === $attributes ) {			// Some plugins are doing things like [name] <[email]>.			if ( 1 === preg_match( '%^<\s*\[\[?[^\[\]]+\]%', $element ) ) {				$element = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $element );			}			// Looks like we found some crazy unfiltered HTML.  Skipping it for sanity.			$element = strtr( $element, $trans );			continue;		}		// Get element name		$front   = array_shift( $attributes );		$back    = array_pop( $attributes );		$matches = array();		preg_match( '%[a-zA-Z0-9]+%', $front, $matches );		$elname = $matches[0];		// Look for shortcodes in each attribute separately.		foreach ( $attributes as &$attr ) {			$open  = strpos( $attr, '[' );			$close = strpos( $attr, ']' );			if ( false === $open || false === $close ) {				continue; // Go to next attribute.  Square braces will be escaped at end of loop.			}			$double = strpos( $attr, '"' );			$single = strpos( $attr, "'" );			if ( ( false === $single || $open < $single ) && ( false === $double || $open < $double ) ) {				// $attr like '[shortcode]' or 'name = [shortcode]' implies unfiltered_html.				// In this specific situation we assume KSES did not run because the input				// was written by an administrator, so we should avoid changing the output				// and we do not need to run KSES here.				$attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr );			} else {				// $attr like 'name = "[shortcode]"' or "name = '[shortcode]'"				// We do not know if $content was unfiltered. Assume KSES ran before shortcodes.				$count    = 0;				$new_attr = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $attr, -1, $count );				if ( $count > 0 ) {					// Sanitize the shortcode output using KSES.					$new_attr = wp_kses_one_attr( $new_attr, $elname );					if ( '' !== trim( $new_attr ) ) {						// The shortcode is safe to use now.						$attr = $new_attr;					}				}			}		}		$element = $front . implode( '', $attributes ) . $back;		// Now encode any remaining [ or ] chars.		$element = strtr( $element, $trans );	}	$content = implode( '', $textarr );	return $content;}/** * Remove placeholders added by do_shortcodes_in_html_tags(). * * @since 4.2.3 * * @param string $content Content to search for placeholders. * @return string Content with placeholders removed. */function unescape_invalid_shortcodes( $content ) {	// Clean up entire string, avoids re-parsing HTML.	$trans = array(		'[' => '[',		']' => ']',	);	$content = strtr( $content, $trans );	return $content;}/** * Retrieve the shortcode attributes regex. * * @since 4.4.0 * * @return string The shortcode attribute regular expression */function get_shortcode_atts_regex() {	return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/';}/** * Retrieve all attributes from the shortcodes tag. * * The attributes list has the attribute name as the key and the value of the * attribute as the value in the key/value pair. This allows for easier * retrieval of the attributes, since all attributes have to be known. * * @since 2.5.0 * * @param string $text * @return array|string List of attribute values. *                      Returns empty array if trim( $text ) == '""'. *                      Returns empty string if trim( $text ) == ''. *                      All other matches are checked for not empty(). */function shortcode_parse_atts( $text ) {	$atts    = array();	$pattern = get_shortcode_atts_regex();	$text    = preg_replace( "/[\x{00a0}\x{200b}]+/u", ' ', $text );	if ( preg_match_all( $pattern, $text, $match, PREG_SET_ORDER ) ) {		foreach ( $match as $m ) {			if ( ! empty( $m[1] ) ) {				$atts[ strtolower( $m[1] ) ] = stripcslashes( $m[2] );			} elseif ( ! empty( $m[3] ) ) {				$atts[ strtolower( $m[3] ) ] = stripcslashes( $m[4] );			} elseif ( ! empty( $m[5] ) ) {				$atts[ strtolower( $m[5] ) ] = stripcslashes( $m[6] );			} elseif ( isset( $m[7] ) && strlen( $m[7] ) ) {				$atts[] = stripcslashes( $m[7] );			} elseif ( isset( $m[8] ) && strlen( $m[8] ) ) {				$atts[] = stripcslashes( $m[8] );			} elseif ( isset( $m[9] ) ) {				$atts[] = stripcslashes( $m[9] );			}		}		// Reject any unclosed HTML elements.		foreach ( $atts as &$value ) {			if ( false !== strpos( $value, '<' ) ) {				if ( 1 !== preg_match( '/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value ) ) {					$value = '';				}			}		}	} else {		$atts = ltrim( $text );	}	return $atts;}/** * Combine user attributes with known attributes and fill in defaults when needed. * * The pairs should be considered to be all of the attributes which are * supported by the caller and given as a list. The returned attributes will * only contain the attributes in the $pairs list. * * If the $atts list has unsupported attributes, then they will be ignored and * removed from the final returned list. * * @since 2.5.0 * * @param array  $pairs     Entire list of supported attributes and their defaults. * @param array  $atts      User defined attributes in shortcode tag. * @param string $shortcode Optional. The name of the shortcode, provided for context to enable filtering * @return array Combined and filtered attribute list. */function shortcode_atts( $pairs, $atts, $shortcode = '' ) {	$atts = (array) $atts;	$out  = array();	foreach ( $pairs as $name => $default ) {		if ( array_key_exists( $name, $atts ) ) {			$out[ $name ] = $atts[ $name ];		} else {			$out[ $name ] = $default;		}	}	/**	 * Filters a shortcode's default attributes.	 *	 * If the third parameter of the shortcode_atts() function is present then this filter is available.	 * The third parameter, $shortcode, is the name of the shortcode.	 *	 * @since 3.6.0	 * @since 4.4.0 Added the `$shortcode` parameter.	 *	 * @param array  $out       The output array of shortcode attributes.	 * @param array  $pairs     The supported attributes and their defaults.	 * @param array  $atts      The user defined shortcode attributes.	 * @param string $shortcode The shortcode name.	 */	if ( $shortcode ) {		$out = apply_filters( "shortcode_atts_{$shortcode}", $out, $pairs, $atts, $shortcode );	}	return $out;}/** * Remove all shortcode tags from the given content. * * @since 2.5.0 * * @global array $shortcode_tags * * @param string $content Content to remove shortcode tags. * @return string Content without shortcode tags. */function strip_shortcodes( $content ) {	global $shortcode_tags;	if ( false === strpos( $content, '[' ) ) {		return $content;	}	if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) {		return $content;	}	// Find all registered tag names in $content.	preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $content, $matches );	$tags_to_remove = array_keys( $shortcode_tags );	/**	 * Filters the list of shortcode tags to remove from the content.	 *	 * @since 4.7.0	 *	 * @param array  $tags_to_remove Array of shortcode tags to remove.	 * @param string $content        Content shortcodes are being removed from.	 */	$tags_to_remove = apply_filters( 'strip_shortcodes_tagnames', $tags_to_remove, $content );	$tagnames = array_intersect( $tags_to_remove, $matches[1] );	if ( empty( $tagnames ) ) {		return $content;	}	$content = do_shortcodes_in_html_tags( $content, true, $tagnames );	$pattern = get_shortcode_regex( $tagnames );	$content = preg_replace_callback( "/$pattern/", 'strip_shortcode_tag', $content );	// Always restore square braces so we don't break things like <!--[if IE ]>	$content = unescape_invalid_shortcodes( $content );	return $content;}/** * Strips a shortcode tag based on RegEx matches against post content. * * @since 3.3.0 * * @param array $m RegEx matches against post content. * @return string|false The content stripped of the tag, otherwise false. */function strip_shortcode_tag( $m ) {	// allow [[foo]] syntax for escaping a tag	if ( $m[1] == '[' && $m[6] == ']' ) {		return substr( $m[0], 1, -1 );	}	return $m[1] . $m[6];}
 |