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