| 296 | * Shortcode placeholder regex. |
| 297 | * Simpler, compatible with above for normal "[shortcode n]" placeholders, |
| 298 | * plus matches shortcode in html placeholders "<tag[shortcode n!]>" for KSES processing. |
| 299 | */ |
| 300 | function get_shortcode_placeholder_regex( $tagnames = null, $exclude_html_tag_regex = false ) { |
| 301 | global $shortcode_tags; |
| 302 | |
| 303 | if ( $tagnames === null ) { |
| 304 | $tagnames = array_keys( $shortcode_tags ); |
| 305 | } |
| 306 | if ( ! $tagnames ) { |
| 307 | return ''; |
| 308 | } |
| 309 | |
| 310 | $tagregexp = get_shortcode_regex( $tagnames, true /*$tagregexp_only*/ ); |
| 311 | |
| 312 | $html_tag_regex = $exclude_html_tag_regex ? '' : '<[A-Za-z][A-Za-z0-9]*\[(?:' . $tagregexp . ') \d+!]>' . '|'; |
| 313 | |
| 314 | return |
| 315 | $html_tag_regex |
| 316 | . '\[' // Opening bracket |
| 317 | . '()' // 1: (null for compatibility with get_shortcode_regex()) |
| 318 | . "($tagregexp)" // 2: Shortcode name |
| 319 | . ' ' // Followed by space |
| 320 | . '(\d+)' // 3: number |
| 321 | . '()' // 4: (null for compatibility with get_shortcode_regex()) |
| 322 | . ']' // Then closing bracket |
| 323 | . '(?:' |
| 324 | . '(' // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags |
| 325 | . '[^[]*+' // Not an opening bracket |
| 326 | . '(?:' |
| 327 | . '\[(?!\/\\2])' // An opening bracket not followed by the closing shortcode tag |
| 328 | . '[^[]*+' // Not an opening bracket |
| 329 | . ')*+' |
| 330 | . ')' |
| 331 | . '\[\/\\2]' // Closing shortcode tag |
| 332 | . ')?' |
| 333 | . '()'; // 6: (null for compatibility with get_shortcode_regex()) |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Return registered shortcode tagnames, reduced to those in $text if given. |
| 338 | */ |
| 339 | function get_shortcode_tagnames( $text = null ) { |
| 340 | global $shortcode_tags; |
| 341 | |
| 342 | $tagnames = array_keys( $shortcode_tags ); |
| 343 | if ( $text !== null ) { |
| 344 | preg_match_all( '/\[\K[^][\s\/]++/', $text, $matches ); |
| 345 | // Reduce to registered shortcodes. |
| 346 | $tagnames = array_values( array_intersect( array_unique( $matches[0] ), $tagnames ) ); |
| 347 | } |
| 348 | return $tagnames; |
| 349 | } |
| 350 | |
| 351 | /** |
| 368 | $current_filter = current_filter(); |
| 369 | |
| 370 | // Check for in html placeholder ("<tag[shortcode n!]>"). |
| 371 | if ( ! $in_html_tags && '<' === $m[0][0] && isset( $wp_placeholder_shortcode_in_html_tags[$current_filter][$m[0]] ) ) { |
| 372 | $in_html_tags = true; |
| 373 | $element = $wp_placeholder_shortcode_in_html_tags[$current_filter][$m[0]]; |
| 374 | $pattern = get_shortcode_placeholder_regex( get_shortcode_tagnames( $element ), true /*$exclude_html_tag_regex*/ ); |
| 375 | $attributes = wp_kses_attr_parse( $element ); |
| 376 | if ( false === $attributes ) { |
| 377 | // Some plugins are doing things like [name] <[email]>. |
| 378 | if ( 1 === preg_match( '%^<\s*\[\[?[^\[\]]+\]%', $element ) ) { |
| 379 | $element = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $element ); |
| 380 | } |
| 381 | } else { |
| 382 | |
| 383 | // Get element name |
| 384 | $front = array_shift( $attributes ); |
| 385 | $back = array_pop( $attributes ); |
| 386 | $matches = array(); |
| 387 | preg_match('%[a-zA-Z0-9]+%', $front, $matches); |
| 388 | $elname = $matches[0]; |
| 389 | |
| 390 | // Look for shortcodes in each attribute separately. |
| 391 | foreach ( $attributes as &$attr ) { |
| 392 | $open = strpos( $attr, '[' ); |
| 393 | $close = strpos( $attr, ']' ); |
| 394 | if ( false === $open || false === $close ) { |
| 395 | continue; // Go to next attribute. Square braces will be escaped at end of loop. |
| 396 | } |
| 397 | $double = strpos( $attr, '"' ); |
| 398 | $single = strpos( $attr, "'" ); |
| 399 | if ( ( false === $single || $open < $single ) && ( false === $double || $open < $double ) ) { |
| 400 | // $attr like '[shortcode]' or 'name = [shortcode]' implies unfiltered_html. |
| 401 | // In this specific situation we assume KSES did not run because the input |
| 402 | // was written by an administrator, so we should avoid changing the output |
| 403 | // and we do not need to run KSES here. |
| 404 | $attr = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $attr ); |
| 405 | } else { |
| 406 | // $attr like 'name = "[shortcode]"' or "name = '[shortcode]'" |
| 407 | // We do not know if $content was unfiltered. Assume KSES ran before shortcodes. |
| 408 | $count = 0; |
| 409 | $new_attr = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $attr, -1, $count ); |
| 410 | if ( $count > 0 ) { |
| 411 | // Sanitize the shortcode output using KSES. |
| 412 | $new_attr = wp_kses_one_attr( $new_attr, $elname ); |
| 413 | if ( '' !== trim( $new_attr ) ) { |
| 414 | // The shortcode is safe to use now. |
| 415 | $attr = $new_attr; |
| 416 | } |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | $element = $front . implode( '', $attributes ) . $back; |
| 421 | } |
| 422 | $in_html_tags = false; |
| 423 | return $element; |
| 424 | } elseif ( $m[3] !== '' && isset( $wp_placeholder_shortcode_matches[$current_filter][$m[2] . ' '. $m[3]] ) ) { // Else if normal shortcode placeholder ("[shortcode n]"). |
| 425 | $m = $wp_placeholder_shortcode_matches[$current_filter][$m[2] . ' '. $m[3]]; |
| 426 | } |
| 427 | |
| 695 | |
| 696 | // Placeholder globals. |
| 697 | $wp_placeholder_idx = 0; |
| 698 | $wp_placeholder_replaces = $wp_placeholder_shortcode_matches = $wp_placeholder_shortcode_in_html_tags = array(); |
| 699 | |
| 700 | add_filter( 'the_content', 'wp_placeholder_shortcodes', 7 ); |
| 701 | //add_filter( 'the_content', 'wp_placeholder_comments', 7 ); |
| 702 | //add_filter( 'the_content', 'wp_placeholder_blobs', 7 ); |
| 703 | add_filter( 'the_content', 'wp_placeholder_restore', 14 ); |
| 704 | |
| 705 | /** |
| 706 | * Filter to replace shortcodes "[shortcode param=blah]" with placeholders "[shortcode n]". |
| 707 | * Also replaces shortcodes in html tags with placeholders "<tag[shortcode n!]>". |
| 708 | */ |
| 709 | function wp_placeholder_shortcodes( $text ) { |
| 710 | global $wp_placeholder_replaces, $wp_placeholder_shortcode_in_html_tags; |
| 711 | |
| 712 | if ( ( $tagnames = get_shortcode_tagnames( $text ) ) && ( $shortcode_regex = get_shortcode_regex( $tagnames ) ) ) { |
| 713 | |
| 714 | // Replace shortcodes with placeholders of the form "[shortcode n]". |
| 715 | // Only the opening shortcode (with or without params, escaped or unescaped) is replaced, any ending shortcode ("[/shortcode]") or enclosed content isn't. |
| 716 | $text = preg_replace_callback( '/' . $shortcode_regex . '/s', 'wp_placeholder_shortcode_callback', $text ); |
| 717 | |
| 718 | // For security reasons, shortcodes in tags need to be passed through KSES. Do some pre-processing to make this possible for do_shortcode_tag(). |
| 719 | |
| 720 | // Only match attributes like 'name = "[shortcode]"' or "name = '[shortcode]'" as attributes like '[shortcode]' or 'name = [shortcode]' implies unfiltered_html. |
| 721 | $shortcode = '(\[(?:' . join( '|', array_map( 'preg_quote', $tagnames ) ) . ') \d+])'; |
| 722 | $attr_with_shortcode = '(?:' . '"[^"]*' . $shortcode . '[^"]*"' . "|'[^']*" . $shortcode . "[^']*'" . ')+'; |
| 723 | $possible_attrs = '(?:[^>"\']|"[^"]*"|\'[^\']*\')*'; |
| 724 | |
| 725 | $html_regex = '<[A-Za-z][A-Za-z\d]*\s+' . $possible_attrs . $attr_with_shortcode . $possible_attrs . '>'; |
| 726 | |
| 727 | // Replace tags containing shortcodes with placeholders of the form "<tag[shortcode n!]>". |
| 728 | $text = preg_replace_callback( '/' . $html_regex . '/s', 'wp_placeholder_shortcode_in_html_tags_callback', $text ); |
| 729 | } |
| 730 | |
| 731 | return $text; |
| 732 | } |
| 733 | |
| 734 | /** |
| 735 | * preg_replace callback to replace shortcode "[shortcode param=blah]" with placeholder "[shortcode n]". |
| 736 | */ |
| 737 | function wp_placeholder_shortcode_callback( $matches ) { |
| 738 | global $wp_placeholder_idx, $wp_placeholder_replaces, $wp_placeholder_shortcode_matches; |
| 739 | |
| 740 | $repl_shortcode = $matches[2] . ' ' . $wp_placeholder_idx++; |
| 741 | if ( $matches[5] !== '' ) { |
| 742 | // Recurse into content. |
| 743 | if ( ( $tagnames = get_shortcode_tagnames( $matches[5] ) ) && ( $shortcode_regex = get_shortcode_regex( $tagnames ) ) ) { |
| 744 | $matches[5] = preg_replace_callback( '/' . $shortcode_regex . '/s', 'wp_placeholder_shortcode_callback', $matches[5] ); |
| 745 | } |
| 746 | $repl = '[' . $repl_shortcode . ']' . $matches[5] . '[/' . $matches[2] . ']'; |
| 747 | } else { |
| 748 | $repl = '[' . $repl_shortcode . ']'; |
| 749 | } |
| 750 | $current_filter = current_filter(); |
| 751 | $wp_placeholder_replaces[$current_filter]['['][$repl] = $matches[0]; |
| 752 | $wp_placeholder_shortcode_matches[$current_filter][$repl_shortcode] = $matches; |
| 753 | |
| 754 | return $repl; |
| 755 | } |
| 756 | |
| 757 | /** |
| 758 | * preg_replace callback to replace html tag "<tag attrib... [shortcode n1] ...attrib... [shortcode n2]...>" with placeholder "<tag[shortcode n1!]>". |
| 759 | */ |
| 760 | function wp_placeholder_shortcode_in_html_tags_callback( $matches ) { |
| 761 | global $wp_placeholder_idx, $wp_placeholder_replaces, $wp_placeholder_shortcode_in_html_tags; |
| 762 | |
| 763 | $shortcode = ''; |
| 764 | if ( $matches[1] !== '' ) { |
| 765 | $shortcode = $matches[1]; |
| 766 | } elseif ( $matches[2] !== '' ) { |
| 767 | $shortcode = $matches[2]; |
| 768 | } elseif ( $matches[3] !== '' ) { |
| 769 | $shortcode = $matches[3]; |
| 770 | } |
| 771 | if ( $shortcode ) { |
| 772 | $html_tag = substr( $matches[0], 0, strpos( $matches[0], ' ' ) ); |
| 773 | $repl = $html_tag . substr( $shortcode, 0, -1 ) . '!]>'; |
| 774 | $current_filter = current_filter(); |
| 775 | $wp_placeholder_replaces[$current_filter]['<'][$repl] = $matches[0]; |
| 776 | $wp_placeholder_shortcode_in_html_tags[$current_filter][$repl] = $matches[0]; |
| 777 | } else { |
| 778 | $repl = $matches[0]; |
| 779 | } |
| 780 | |
| 781 | return $repl; |
| 782 | } |
| 783 | |
| 784 | /* |
| 785 | * Filter to replace comment tags with placeholders "<!--cn-->". |
| 786 | */ |
| 787 | function wp_placeholder_comments( $text ) { |
| 788 | $comment_regex = |
| 789 | '<!' // Start of comment. |
| 790 | . '(?:' // Unroll the loop: Consume everything until --> is found. |
| 791 | . '-(?!->)' // Dash not followed by end of comment. |
| 792 | . '[^\-]*+' // Consume non-dashes. |
| 793 | . ')*+' // Loop possessively. |
| 794 | . '(?:-->)?'; // End of comment. If not found, match all input. |
| 795 | |
| 796 | $text = preg_replace_callback( '/' . $comment_regex . '/s', 'wp_placeholder_comment_callback', $text ); |
| 797 | |
| 798 | return $text; |
| 799 | } |
| 800 | |
| 801 | /** |
| 802 | * preg_replace callback to replace comment html "<!--blah-->" with placeholder "<!--cn-->". |
| 803 | */ |
| 804 | function wp_placeholder_comment_callback( $matches ) { |
| 805 | return wp_placeholder_prefix_callback( $matches, 'c' ); |
| 806 | } |
| 807 | |
| 808 | /* |
| 809 | * Filter to replace blob-like html tags with placeholders "<!--bn-->". |
| 810 | */ |
| 811 | function wp_placeholder_blobs( $text ) { |
| 812 | $blob_regex = '<(audio|input|object|script|select|style|textarea|video)(?:(?:[^>"\']*|"[^"]*"|\'[^\']*\')*)?' . '>.*?<\/\\1\s*>'; |
| 813 | |
| 814 | $text = preg_replace_callback( '/' . $blob_regex . '/s', 'wp_placeholder_blob_callback', $text ); |
| 815 | |
| 816 | return $text; |
| 817 | } |
| 818 | |
| 819 | /** |
| 820 | * preg_replace callback to replace blob-like html "<script ...>...</script>" with placeholder "<!--bn-->". |
| 821 | */ |
| 822 | function wp_placeholder_blob_callback( $matches ) { |
| 823 | return wp_placeholder_prefix_callback( $matches, 'b' ); |
| 824 | } |
| 825 | |
| 826 | /** |
| 827 | * Helper to do comment-like placeholders with prefix ("<!--pn-->"). |
| 828 | */ |
| 829 | function wp_placeholder_prefix_callback( $matches, $prefix ) { |
| 830 | global $wp_placeholder_idx, $wp_placeholder_replaces; |
| 831 | |
| 832 | $repl = '<!--' . $prefix . $wp_placeholder_idx++ . '-->'; |
| 833 | $wp_placeholder_replaces[current_filter()][$prefix][$repl] = $matches[0]; |
| 834 | |
| 835 | return $repl; |
| 836 | } |
| 837 | |
| 838 | /** |
| 839 | * Restore placeholder original text. |
| 840 | */ |
| 841 | function wp_placeholder_restore( $text, $prefix = null ) { |
| 842 | global $wp_placeholder_replaces; |
| 843 | |
| 844 | $current_filter = current_filter(); |
| 845 | |
| 846 | if ( ! empty( $wp_placeholder_replaces[$current_filter] ) ) { |
| 847 | $replaces = &$wp_placeholder_replaces[$current_filter]; |
| 848 | if ( $prefix ) { |
| 849 | // If given as array then should be careful to reverse ordering of nested replacements. |
| 850 | $pfs = is_array( $prefix ) ? $prefix : array( $prefix ); |
| 851 | foreach ( $pfs as $pf ) { |
| 852 | if ( isset( $replaces[$pf] ) ) { |
| 853 | $text = str_replace( array_keys( $replaces[$pf] ), $replaces[$pf], $text ); |
| 854 | unset( $replaces[$pf] ); |
| 855 | } |
| 856 | } |
| 857 | } else { |
| 858 | // Do it backwards to ensure nested replacements are replaced. |
| 859 | foreach ( array_reverse( $replaces ) as $repl ) { |
| 860 | $text = str_replace( array_keys( $repl ), $repl, $text ); |
| 861 | } |
| 862 | $replaces = array(); |
| 863 | } |
| 864 | } |
| 865 | |
| 866 | return $text; |
| 867 | } |