剖析 WordPress `_wp_die_handler()` 函数的源码:如何处理致命错误并显示友好的错误页面。

各位观众老爷,大家好!今天咱们来聊聊 WordPress 里一个相当重要的“救火队员”—— _wp_die_handler() 函数。 顾名思义,这玩意儿就是专门处理 WordPress 里的“致命错误”的,当程序遇到不可饶恕的错误,没法继续跑下去了,就得靠它来收拾残局,至少要给用户一个体面的错误页面,而不是一片空白或者一堆乱码。

咱们先来大概了解一下 wp_die() 函数的用法,它其实是 _wp_die_handler() 的一个包装器,负责调用 _wp_die_handler() 并传递参数。

wp_die() 的基本用法

wp_die(
    string $message = '',
    string $title = '',
    array|string $args = array()
);
  • $message: 要显示的错误消息,可以是字符串或者 WP_Error 对象。
  • $title: 错误页面的标题。
  • $args: 一个包含各种选项的数组或字符串。 常见的选项有:
    • response: HTTP 响应状态码(默认 500)。
    • back_link: 是否显示返回链接(默认 false)。
    • text_direction: 文字方向(默认由 is_rtl() 函数决定)。
    • exit: 是否在显示错误后立即退出脚本(默认 true)。
    • hook: 一个在错误消息显示之前执行的钩子。

OK,有了 wp_die() 这个“投弹手”,接下来就该看看 _wp_die_handler() 这个“拆弹专家”是怎么工作的了。 咱们直接上源码,一点一点地扒:

_wp_die_handler() 函数源码

(WordPress 6.4.2 版本)

/**
 * Default handler for wp_die().
 *
 * It generates a simple HTML page with the error message.
 *
 * @since 3.0.0
 *
 * @global WP_Error $wp_error
 *
 * @param string|WP_Error $message Error message or WP_Error object.
 * @param string          $title   Optional. Error title. Default empty string.
 * @param array|string    $args    Optional. Array or string of arguments. See {@see wp_die()}.
 */
function _wp_die_handler( $message, $title = '', $args = array() ) {
    global $wp_error;

    $have_gettext = function_exists( '__' );

    $defaults = array(
        'response'    => 500,
        'exit'        => true,
        'back_link'   => false,
        'text_direction' => 'ltr',
    );

    $args = wp_parse_args( $args, $defaults );

    $have_back_link = false;
    if ( $args['back_link'] ) {
        $have_back_link = true;
        if ( true === $args['back_link'] ) {
            $back_text = $have_gettext ? __( '« Back' ) : '« Back';
            $back_link = wp_get_referer();
            if ( ! $back_link ) {
                $back_link = home_url();
            }
        } else {
            $back_text = $args['back_link'];
            $back_link = wp_get_referer();
            if ( ! $back_link ) {
                $back_link = home_url();
            }
        }
    } else {
        $back_text = '';
        $back_link = '';
    }

    if ( is_scalar( $message ) ) {
        $message = "<p>$message</p>";
    } elseif ( is_object( $message ) && is_a( $message, 'WP_Error' ) ) {
        $errors = $message->get_error_messages();
        if ( empty( $errors ) ) {
            $message = '<p>' . ( $have_gettext ? __( 'Something went wrong.' ) : 'Something went wrong.' ) . '</p>';
        } else {
            $message = '<ul>';
            foreach ( $errors as $error ) {
                $message .= '<li>' . $error . '</li>';
            }
            $message .= '</ul>';
        }
    }

    if ( did_action( 'admin_head' ) ) {
        $text_direction = get_user_meta( get_current_user_id(), 'text_direction', true );
        if ( empty( $text_direction ) ) {
            $text_direction = 'ltr';
        }
    } else {
        $text_direction = $args['text_direction'];
    }

    if ( ! wp_doing_ajax() ) {
        @header( sprintf( 'Content-Type: text/html; charset=%s', get_option( 'blog_charset' ) ), true, $args['response'] );
    }

    if ( empty( $title ) ) {
        $title = $have_gettext ? __( 'WordPress &rsaquo; Error' ) : 'WordPress &rsaquo; Error';
    }

    if ( defined( 'XMLRPC_REQUEST' ) || wp_doing_ajax() || isset( $_REQUEST['TB_iframe'] ) ) {
        if ( is_scalar( $message ) ) {
            wp_die( $message, $title, $args );
        } else {
            wp_die( ' ', $title, $args );
        }
    }

    if ( headers_sent() ) {
        $message = '<p>' . ( $have_gettext ? __( 'The site is experiencing technical difficulties.' ) : 'The site is experiencing technical difficulties.' ) . '</p>';
        ?>
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title><?php echo esc_html( $title ); ?></title>
            </head>
            <body>
                <h1><?php echo esc_html( $title ); ?></h1>
                <?php echo $message; ?>
            </body>
            </html>
        <?php
    } else {
        ?>
        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
            <meta name="viewport" content="width=device-width" />
            <title><?php echo esc_html( $title ); ?></title>
            <?php
            wp_enqueue_style( 'wp-die' );
            ?>
        </head>
        <body id="error-page" <?php if ( 'rtl' === $text_direction ) {
            echo ' class="rtl"';
        } ?>>
            <div class="wp-die-message">
                <h1><?php echo esc_html( $title ); ?></h1>
                <?php echo $message; ?>
                <?php if ( $have_back_link ) : ?>
                    <p><a href="<?php echo esc_url( $back_link ); ?>"><?php echo esc_html( $back_text ); ?></a></p>
                <?php endif; ?>
            </div>
        </body>
        </html>
        <?php
    }

    if ( $args['exit'] ) {
        die();
    }
}

逐步解析

  1. 函数签名和文档注释

    function _wp_die_handler( $message, $title = '', $args = array() ) {

    没啥特别的,接收错误消息、标题和参数。文档注释说明了该函数的作用,以及参数的含义。 良好的注释习惯,方便别人(也方便未来的自己)理解代码。

  2. 获取全局变量

    global $wp_error;

    这里获取了全局变量 $wp_error。 虽然这个函数本身不直接使用 $wp_error,但是考虑到可能在其他地方已经设置了这个全局变量,方便后续处理。

  3. 判断是否支持本地化

    $have_gettext = function_exists( '__' );

    __() 函数是 WordPress 的本地化函数,如果存在这个函数,说明 WordPress 已经加载了本地化支持。这很重要,因为错误消息可能需要根据用户的语言进行翻译。

  4. 设置默认参数

    $defaults = array(
        'response'    => 500,
        'exit'        => true,
        'back_link'   => false,
        'text_direction' => 'ltr',
    );
    
    $args = wp_parse_args( $args, $defaults );

    wp_parse_args() 是一个非常有用的函数,它将用户传入的参数 $args 与默认参数 $defaults 合并。 这样,即使用户没有传入某些参数,也会使用默认值,保证程序的正常运行。 这里设置了 HTTP 响应状态码为 500(服务器内部错误),默认退出脚本,不显示返回链接,文字方向为从左到右。

  5. 处理返回链接

    $have_back_link = false;
    if ( $args['back_link'] ) {
        $have_back_link = true;
        if ( true === $args['back_link'] ) {
            $back_text = $have_gettext ? __( '&laquo; Back' ) : '&laquo; Back';
            $back_link = wp_get_referer();
            if ( ! $back_link ) {
                $back_link = home_url();
            }
        } else {
            $back_text = $args['back_link'];
            $back_link = wp_get_referer();
            if ( ! $back_link ) {
                $back_link = home_url();
            }
        }
    } else {
        $back_text = '';
        $back_link = '';
    }

    这段代码处理是否显示返回链接的逻辑。 如果 $args['back_link'] 为 true,则显示一个默认的返回链接,指向用户之前的页面(通过 wp_get_referer() 获取),如果获取不到,则指向首页。 如果 $args['back_link'] 是一个字符串,则将该字符串作为链接文本。

  6. 处理错误消息

    if ( is_scalar( $message ) ) {
        $message = "<p>$message</p>";
    } elseif ( is_object( $message ) && is_a( $message, 'WP_Error' ) ) {
        $errors = $message->get_error_messages();
        if ( empty( $errors ) ) {
            $message = '<p>' . ( $have_gettext ? __( 'Something went wrong.' ) : 'Something went wrong.' ) . '</p>';
        } else {
            $message = '<ul>';
            foreach ( $errors as $error ) {
                $message .= '<li>' . $error . '</li>';
            }
            $message .= '</ul>';
        }
    }

    这段代码处理不同类型的错误消息。

    • 如果 $message 是一个标量(例如字符串或数字),则将其包装在 <p> 标签中。
    • 如果 $message 是一个 WP_Error 对象,则提取错误消息并将其显示为一个无序列表。 如果 WP_Error 对象没有任何错误消息,则显示一个通用的 "Something went wrong." 消息。
  7. 确定文字方向

    if ( did_action( 'admin_head' ) ) {
        $text_direction = get_user_meta( get_current_user_id(), 'text_direction', true );
        if ( empty( $text_direction ) ) {
            $text_direction = 'ltr';
        }
    } else {
        $text_direction = $args['text_direction'];
    }

    这段代码确定文字方向(从左到右或从右到左)。 如果已经执行了 admin_head 钩子(通常在后台管理界面),则从当前用户的用户元数据中获取文字方向。 否则,使用 $args['text_direction'] 中指定的值。

  8. 发送 HTTP 头部

    if ( ! wp_doing_ajax() ) {
        @header( sprintf( 'Content-Type: text/html; charset=%s', get_option( 'blog_charset' ) ), true, $args['response'] );
    }

    这段代码发送 HTTP 头部。 首先,检查是否是 AJAX 请求。 如果不是 AJAX 请求,则发送 Content-Type 头部,指定内容类型为 HTML,字符集为 WordPress 的字符集(通过 get_option( 'blog_charset' ) 获取),并设置 HTTP 响应状态码为 $args['response'] 中指定的值。 @ 符号用于抑制 header() 函数可能产生的错误。

  9. 设置默认标题

    if ( empty( $title ) ) {
        $title = $have_gettext ? __( 'WordPress &rsaquo; Error' ) : 'WordPress &rsaquo; Error';
    }

    如果 $title 为空,则设置一个默认的标题 "WordPress › Error"。

  10. 处理特殊请求

    if ( defined( 'XMLRPC_REQUEST' ) || wp_doing_ajax() || isset( $_REQUEST['TB_iframe'] ) ) {
        if ( is_scalar( $message ) ) {
            wp_die( $message, $title, $args );
        } else {
            wp_die( ' ', $title, $args );
        }
    }

    这段代码处理 XMLRPC 请求、AJAX 请求和 Thickbox iframe 请求。 在这些情况下,直接调用 wp_die() 函数,而不是生成完整的 HTML 页面。 如果是标量消息则直接输出,否则输出一个空字符串。 这样做是为了保证响应的格式符合这些请求的期望。

  11. 处理头部已发送的情况

    if ( headers_sent() ) {
        $message = '<p>' . ( $have_gettext ? __( 'The site is experiencing technical difficulties.' ) : 'The site is experiencing technical difficulties.' ) . '</p>';
        ?>
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title><?php echo esc_html( $title ); ?></title>
            </head>
            <body>
                <h1><?php echo esc_html( $title ); ?></h1>
                <?php echo $message; ?>
            </body>
            </html>
        <?php
    } else {
        ?>
        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
            <meta name="viewport" content="width=device-width" />
            <title><?php echo esc_html( $title ); ?></title>
            <?php
            wp_enqueue_style( 'wp-die' );
            ?>
        </head>
        <body id="error-page" <?php if ( 'rtl' === $text_direction ) {
            echo ' class="rtl"';
        } ?>>
            <div class="wp-die-message">
                <h1><?php echo esc_html( $title ); ?></h1>
                <?php echo $message; ?>
                <?php if ( $have_back_link ) : ?>
                    <p><a href="<?php echo esc_url( $back_link ); ?>"><?php echo esc_html( $back_text ); ?></a></p>
                <?php endif; ?>
            </div>
        </body>
        </html>
        <?php
    }

    这段代码生成 HTML 页面。 它首先检查是否已经发送了 HTTP 头部。

    • 如果已经发送了头部,则生成一个简单的 HTML 页面,只包含标题和错误消息。 由于头部已经发送,所以无法使用 WordPress 的函数来加载样式表。 并且错误信息使用了一个通用的 "The site is experiencing technical difficulties." 消息。
    • 如果还没有发送头部,则生成一个更完整的 HTML 页面,包含 doctype、html 标签、head 标签和 body 标签。 它还加载了 wp-die 样式表,并根据文字方向设置 body 标签的 class 属性。
  12. 退出脚本

    if ( $args['exit'] ) {
        die();
    }

    如果 $args['exit'] 为 true,则使用 die() 函数退出脚本。

总结

_wp_die_handler() 函数的主要功能就是:

  • 接收错误消息、标题和参数。
  • 处理不同类型的错误消息(字符串或 WP_Error 对象)。
  • 设置 HTTP 响应状态码。
  • 生成 HTML 错误页面。
  • 退出脚本。

它考虑了各种情况,例如是否支持本地化、是否是 AJAX 请求、是否已经发送了头部等,力求在各种情况下都能给用户一个友好的错误提示。

一些需要注意的点

  • 安全性: _wp_die_handler() 函数会对输出进行转义,以防止 XSS 攻击。 例如,esc_html() 函数用于转义 HTML 标签,esc_url() 函数用于转义 URL。
  • 可定制性: 可以通过 wp_die_handler 过滤器来替换默认的 _wp_die_handler() 函数,从而实现自定义的错误处理逻辑。
  • 调试: 在开发环境中,可以设置 WP_DEBUG 常量为 true,以便显示更详细的错误信息。

实际应用

咱们来举个例子,假设你的插件在尝试连接数据库时失败了:

<?php
// 尝试连接数据库
$mydb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );

if ( ! $mydb ) {
    wp_die(
        'Failed to connect to the database.',
        'Database Error',
        array(
            'response'  => 503, // 服务不可用
            'back_link' => true, // 显示返回链接
        )
    );
}

这段代码会显示一个错误页面,标题为 "Database Error",错误消息为 "Failed to connect to the database.",HTTP 响应状态码为 503,并显示一个返回链接。

表格总结

功能 描述
参数处理 使用 wp_parse_args() 合并用户传入的参数和默认参数,确保程序的正常运行。
错误消息处理 处理字符串和 WP_Error 对象两种类型的错误消息,并将其格式化为 HTML。
HTTP 头部处理 发送 HTTP 头部,设置内容类型和字符集,以及 HTTP 响应状态码。
HTML 页面生成 生成 HTML 错误页面,包含标题、错误消息和返回链接。 根据是否已经发送头部,生成不同版本的 HTML 页面。
特殊请求处理 处理 XMLRPC 请求、AJAX 请求和 Thickbox iframe 请求,保证响应的格式符合这些请求的期望。
安全性 对输出进行转义,以防止 XSS 攻击。
可定制性 可以通过 wp_die_handler 过滤器来替换默认的错误处理函数。

希望通过今天的讲解,大家对 WordPress 的 _wp_die_handler() 函数有了更深入的了解。 下次遇到 WordPress 报错的时候,也能更加淡定地分析问题,解决问题! 咱们下期再见!

发表回复

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