Make WordPress Core

Ticket #33134: 33134_shortcodes.diff

File 33134_shortcodes.diff, 14.7 KB (added by gitlost, 10 years ago)

Demonstration of the shortcode placeholder pre-processor.

  • src/wp-includes/shortcodes.php

     
    187187 */
    188188function do_shortcode( $content, $ignore_html = false ) {
    189189        global $shortcode_tags;
     190        global $wp_placeholder_shortcode_matches;
    190191
    191192        if ( false === strpos( $content, '[' ) ) {
    192193                return $content;
     
    195196        if (empty($shortcode_tags) || !is_array($shortcode_tags))
    196197                return $content;
    197198
    198         $tagnames = array_keys($shortcode_tags);
    199         $tagregexp = join( '|', array_map('preg_quote', $tagnames) );
    200         $pattern = "/\\[($tagregexp)/s";
     199        if ( ! empty( $wp_placeholder_shortcode_matches[current_filter()] ) ) {
     200                if ( $pattern = get_shortcode_placeholder_regex( get_shortcode_tagnames( $content ) ) ) {
     201                        $content = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );
     202                }
     203        } else {
     204                $tagnames = array_keys($shortcode_tags);
     205                $tagregexp = join( '|', array_map('preg_quote', $tagnames) );
     206                $pattern = "/\\[($tagregexp)/s";
    201207
    202         if ( 1 !== preg_match( $pattern, $content ) ) {
    203                 // Avoids parsing HTML when there are no shortcodes or embeds anyway.
    204                 return $content;
    205         }
     208                if ( 1 !== preg_match( $pattern, $content ) ) {
     209                        // Avoids parsing HTML when there are no shortcodes or embeds anyway.
     210                        return $content;
     211                }
    206212
    207         $content = do_shortcodes_in_html_tags( $content, $ignore_html );
     213                $content = do_shortcodes_in_html_tags( $content, $ignore_html );
    208214
    209         $pattern = get_shortcode_regex();
    210         $content = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );
     215                $pattern = get_shortcode_regex();
     216                $content = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );
    211217
    212         // Always restore square braces so we don't break things like <!--[if IE ]>
    213         $content = unescape_invalid_shortcodes( $content );
     218                // Always restore square braces so we don't break things like <!--[if IE ]>
     219                $content = unescape_invalid_shortcodes( $content );
     220        }
    214221
    215222        return $content;
    216223}
     
    236243 *
    237244 * @return string The shortcode search regular expression
    238245 */
    239 function get_shortcode_regex() {
     246function get_shortcode_regex( $tagnames = null, $tagregexp_only = false ) {
    240247        global $shortcode_tags;
    241         $tagnames = array_keys($shortcode_tags);
     248
     249        if ( $tagnames === null ) {
     250                $tagnames = array_keys( $shortcode_tags );
     251        }
     252        if ( ! $tagnames ) {
     253                return '';
     254        }
     255
    242256        $tagregexp = join( '|', array_map('preg_quote', $tagnames) );
    243257
     258        if ( $tagregexp_only ) {
     259                return $tagregexp;
     260        }
     261
    244262        // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag()
    245263        // Also, see shortcode_unautop() and shortcode.js.
    246264        return
     
    275293}
    276294
    277295/**
     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 */
     300function 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 */
     339function 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/**
    278352 * Regular Expression callable for do_shortcode() for calling shortcode hook.
    279353 * @see get_shortcode_regex for details of the match array contents.
    280354 *
     
    288362 */
    289363function do_shortcode_tag( $m ) {
    290364        global $shortcode_tags;
     365        global $wp_placeholder_shortcode_matches, $wp_placeholder_shortcode_in_html_tags;
     366        static $in_html_tags = false;
    291367
     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
    292428        // allow [[foo]] syntax for escaping a tag
    293429        if ( $m[1] == '[' && $m[6] == ']' ) {
    294430                return substr($m[0], 1, -1);
     
    556692
    557693        return $m[1] . $m[6];
    558694}
     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
     700add_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 );
     703add_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 */
     709function 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 */
     737function 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 */
     760function 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 */
     787function 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 */
     804function 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 */
     811function 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 */
     822function 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 */
     829function 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 */
     841function 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}