分析 WordPress `do_shortcode()` 函数的源码:如何解析和执行短代码,并支持嵌套。

各位代码界的探险家们,早上好!今天咱们要深入WordPress的心脏地带,一起扒一扒do_shortcode()这个神奇的函数,看看它是如何像一位优秀的魔术师一样,把那些看似简单的短代码变成功能强大的魔法。准备好了吗?让我们开始这场代码考古之旅!

一、短代码的起源故事:为什么要搞短代码?

想象一下,你是一位WordPress博主,想在文章里插入一个漂亮的相册,或者一个复杂的表格。如果让你每次都手动写HTML代码,那简直是场噩梦!于是,短代码应运而生。它们就像是一些预定义的“快捷方式”,用简单的标签包裹起来,让你可以轻松地插入复杂的功能,而无需编写大量的HTML或PHP代码。

例如:

[my_gallery ids="1,2,3,4,5"]

这段短短的代码,可能背后藏着一个完整的相册功能!

二、do_shortcode():短代码的“翻译器”

do_shortcode()函数是WordPress短代码机制的核心。它的主要任务就是:

  1. 扫描: 在给定的字符串中查找短代码。
  2. 解析: 提取短代码的标签和属性。
  3. 执行: 调用与该标签关联的函数(也就是短代码的回调函数)。
  4. 替换: 将短代码替换为回调函数的返回值。

简单来说,它就像一位勤劳的翻译官,把那些神秘的短代码“翻译”成浏览器能够理解的HTML代码。

三、源码剖析:do_shortcode()的内部结构

让我们一起深入wp-includes/shortcodes.php文件,揭开do_shortcode()的神秘面纱。

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

    // Avoid nesting this function when already being processed.
    static $doing_shortcode_field;

    if ( true === $doing_shortcode_field ) {
        return $content;
    }

    $doing_shortcode_field = true;

    //preg_match_all( '@[([^<>&/[]x00-x20=]++)@', $content, $matches, PREG_OFFSET_CAPTURE );
    $pattern = get_shortcode_regex();
    $content = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );

    // Always restore doing_shortcode_field, regardless.
    $doing_shortcode_field = false;

    return $content;
}

看起来是不是有点复杂?别怕,咱们一步步来拆解:

  • global $shortcode_tags;: 这一行代码非常重要。$shortcode_tags是一个全局数组,它存储了所有已注册的短代码及其对应的回调函数。 也就是说,WordPress就是通过这个数组来知道哪个短代码对应哪个函数。

  • if ( false === strpos( $content, '[' ) ) { return $content; }: 这是一个快速检查。如果内容中根本没有[,那就说明没有短代码,直接返回原始内容,避免不必要的处理。

  • if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) { return $content; }: 再次检查,确保$shortcode_tags数组存在并且不为空。如果没有注册任何短代码,也直接返回。

  • static $doing_shortcode_field;: 这是一个静态变量,用于防止短代码的无限嵌套调用。如果do_shortcode()函数正在执行中,再次调用它会直接返回,避免栈溢出。

  • $pattern = get_shortcode_regex();: 这是一个关键步骤。get_shortcode_regex()函数会生成一个用于匹配短代码的正则表达式。这个正则表达式非常强大,可以匹配各种形式的短代码,包括带有属性的、自闭合的等等。咱们待会会详细分析这个函数。

  • $content = preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );: 这才是真正的“翻译”过程!preg_replace_callback()函数会使用正则表达式 $pattern 在内容 $content 中查找所有匹配的短代码,并对每个匹配到的短代码调用 do_shortcode_tag() 函数进行处理。

  • $doing_shortcode_field = false;: 处理完毕后,重置静态变量,允许后续的短代码处理。

四、get_shortcode_regex():短代码的“雷达”

让我们看看get_shortcode_regex()函数是如何构建那个强大的正则表达式的。

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_shortcodes()
    // Also, see shortcode_unautop() and shortcode.js.
    return
        '\['                              // Opening bracket
        . '(\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
        . "($tagregexp)"                     // 2: Shortcode name
        . '(?![\w-])'                       // Word boundary
        . '('                                // 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]]
}

这个正则表达式看起来像一堆乱码,但其实它非常精妙。让我们来解释一下它的关键部分:

  • '\[': 匹配开头的方括号 [
  • (\[?): 匹配可选的第二个开头的方括号,用于转义短代码,例如 [[my_shortcode]]
  • ($tagregexp): 匹配短代码的标签名。$tagregexp 是一个由所有已注册的短代码标签名组成的正则表达式,例如 (my_gallery|my_table|...)
  • (?![\w-]): 确保标签名后面不是字母、数字或下划线,这是一个词语边界的判断,避免误匹配。
  • *`([^]/] … )**: 这部分匹配短代码的属性。它允许属性中包含除了]/` 之外的任何字符。
  • (\/)\]: 匹配自闭合的短代码,例如 [my_shortcode /]
  • \](?: ... )?: 匹配闭合的短代码,例如 [my_shortcode] content [/my_shortcode]
  • *`([^[]+(?:[(?!/2])[^[]+)+)`**: 匹配短代码的内容,允许内容中包含嵌套的短代码,但要排除与当前短代码标签相同的闭合标签。
  • \[\/\2\]: 匹配闭合的短代码标签。
  • (\]?): 匹配可选的第二个闭合的方括号,用于转义短代码,例如 [[my_shortcode]]

这个正则表达式考虑了各种情况,包括:

  • 普通短代码: [my_shortcode]
  • 带有属性的短代码: [my_shortcode attribute1="value1" attribute2="value2"]
  • 自闭合短代码: [my_shortcode /]
  • 带有内容的短代码: [my_shortcode] content [/my_shortcode]
  • 嵌套短代码: [my_shortcode] content [another_shortcode] inner content [/another_shortcode] [/my_shortcode]
  • 转义短代码: [[my_shortcode]]

五、do_shortcode_tag():短代码的“执行者”

do_shortcode_tag() 函数是真正执行短代码的地方。它接收 preg_replace_callback() 函数匹配到的短代码信息,并调用相应的回调函数。

function do_shortcode_tag( $matches ) {
    global $shortcode_tags;

    // Allow shortcodes to be redefined.
    $tag = $matches[2];
    if ( isset( $shortcode_tags[ $tag ] ) ) {
        $func = $shortcode_tags[ $tag ];

        $atts = shortcode_parse_atts( $matches[3] );

        if ( is_callable( $func ) ) {
            $out = call_user_func( $func, $atts, ! empty( $matches[5] ) ? $matches[5] : null, $tag );
        } else {
            $out = '';
        }

        if ( ! empty( $out ) ) {
            return $matches[1] . $out . $matches[6];
        } else {
            return $matches[0];
        }
    } else {
        return $matches[0];
    }
}

让我们逐行分析:

  • $tag = $matches[2];: 从匹配结果中提取短代码的标签名。$matches[2] 对应于 get_shortcode_regex() 函数中定义的第二个捕获组,也就是标签名。

  • if ( isset( $shortcode_tags[ $tag ] ) ) { ... }: 检查该标签名是否已注册。

  • $func = $shortcode_tags[ $tag ];: 获取与该标签名关联的回调函数。

  • $atts = shortcode_parse_atts( $matches[3] );: 解析短代码的属性。$matches[3] 对应于 get_shortcode_regex() 函数中定义的第三个捕获组,也就是属性字符串。shortcode_parse_atts() 函数会将属性字符串解析成一个关联数组,例如 attribute1="value1" attribute2="value2" 会被解析成 ['attribute1' => 'value1', 'attribute2' => 'value2']

  • if ( is_callable( $func ) ) { ... }: 确保回调函数是可调用的。

  • $out = call_user_func( $func, $atts, ! empty( $matches[5] ) ? $matches[5] : null, $tag );: 调用回调函数,并将属性数组、内容(如果存在)和标签名作为参数传递给它。 $matches[5] 对应于 get_shortcode_regex() 函数中定义的第五个捕获组,也就是短代码的内容。

  • return $matches[1] . $out . $matches[6];: 将短代码替换为回调函数的返回值。$matches[1]$matches[6] 对应于转义短代码时使用的可选方括号。

六、shortcode_parse_atts():属性的“分解者”

shortcode_parse_atts() 函数负责将短代码的属性字符串解析成一个关联数组。

function shortcode_parse_atts( $text ) {
    $atts    = array();
    $pattern = '/([w-]+)s*=s*"([^"]*)"(?:s|$)|([w-]+)s*=s*'([^']*)'(?:s|$)|([w-]+)s*=s*([^s'"]+)(?:s|$)|"([^"]*)"(?:s|$)|(S+)(?:s|$)/';
    $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 ( ! empty( $m[7] ) ) {
                $atts[] = stripcslashes( $m[7] );
            } elseif ( ! empty( $m[8] ) ) {
                $atts[] = stripcslashes( $m[8] );
            }
        }
    } else {
        $atts = ltrim( $text );
    }
    return $atts;
}

这个函数使用正则表达式来匹配属性,支持以下几种形式:

  • attribute="value" (双引号)
  • attribute='value' (单引号)
  • attribute=value (无引号)
  • "value" (匿名属性,值被添加到数组中)
  • value (匿名属性,值被添加到数组中)

七、短代码的注册与使用

要使用短代码,首先需要注册它。可以使用 add_shortcode() 函数来注册一个短代码及其对应的回调函数。

function my_shortcode_callback( $atts, $content = null, $tag = '' ) {
    // 处理属性
    $atts = shortcode_atts(
        array(
            'id' => 0,
            'title' => 'Default Title',
        ),
        $atts,
        $tag
    );

    $id = intval( $atts['id'] );
    $title = esc_attr( $atts['title'] );

    // 构建输出
    $output = '<div class="my-shortcode">';
    $output .= '<h2>' . $title . '</h2>';
    $output .= '<p>ID: ' . $id . '</p>';
    if ( ! is_null( $content ) ) {
        $output .= '<p>Content: ' . do_shortcode( $content ) . '</p>';
    }
    $output .= '</div>';

    return $output;
}
add_shortcode( 'my_shortcode', 'my_shortcode_callback' );

这个例子注册了一个名为 my_shortcode 的短代码,它接受 idtitle 两个属性,并输出一个带有标题和ID的 div 元素。 shortcode_atts() 函数用于设置属性的默认值。 注意,如果短代码有内容,并且内容中可能包含其他短代码,需要使用 do_shortcode( $content ) 来处理内容。

然后在文章或页面中使用这个短代码:

[my_shortcode id="123" title="My Custom Title"]This is the content.[/my_shortcode]

八、嵌套短代码的处理

do_shortcode() 函数本身支持嵌套短代码。当它遇到一个短代码时,会递归地调用自身来处理嵌套的短代码。 这样就可以实现非常复杂的功能。

例如:

[outer_shortcode]
    [inner_shortcode attribute="value"]Inner Content[/inner_shortcode]
[/outer_shortcode]

在这个例子中,do_shortcode() 函数会先处理 outer_shortcode,然后在处理 outer_shortcode 的回调函数时,会再次调用 do_shortcode() 函数来处理 inner_shortcode

九、短代码的安全性

短代码非常方便,但也存在安全风险。如果允许用户随意使用短代码,可能会导致安全漏洞,例如跨站脚本攻击(XSS)。

因此,需要注意以下几点:

  • 验证和转义属性: 确保属性值是安全的,可以使用 esc_attr()esc_url() 等函数来转义属性值。
  • 限制短代码的使用: 只允许受信任的用户使用短代码。
  • 避免执行敏感操作: 不要在短代码的回调函数中执行敏感操作,例如数据库修改或文件操作。

十、总结与展望

do_shortcode() 函数是WordPress短代码机制的核心,它通过正则表达式匹配短代码,解析属性,并调用相应的回调函数来执行短代码。 短代码机制非常灵活,可以用于实现各种复杂的功能,但同时也需要注意安全风险。

希望今天的讲座能够帮助你更深入地理解WordPress的短代码机制。 掌握了这些知识,你就可以像一位真正的代码魔术师一样,创造出各种神奇的WordPress功能! 记住,代码的世界充满无限可能,勇敢地去探索吧!

今天的课程就到这里,各位探险家们,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注