分析 WordPress `wp_fix_server_vars()` 函数的源码:如何标准化 `$_SERVER` 变量以确保兼容性。

各位观众老爷,早上好/中午好/晚上好! 欢迎来到今天的“WordPress 源码扒皮”特别节目。今天我们要聊聊一个藏得很深,但又非常重要的函数:wp_fix_server_vars()。 它的作用,简单来说,就是“擦屁股”,哦不,是“标准化” $_SERVER 全局变量,让 WordPress 在各种服务器环境下都能愉快地玩耍。

我知道,一提到 $_SERVER,大家可能就觉得头大,这玩意儿里面塞满了各种服务器信息,乱七八糟的。 不同服务器、不同配置,里面的内容还不一样,简直是 PHP 世界的百慕大。 但 WordPress 作为一个要运行在各种服务器上的 CMS,必须对这些变量进行统一处理,才能保证代码的兼容性和稳定性。

所以,wp_fix_server_vars() 就诞生了。 它的任务,就是把 $_SERVER 这个百慕大,变成一个可预测、可控制的花园。

一、 为什么要标准化 $_SERVER

在深入源码之前,我们先来聊聊为什么要这么做。 想象一下,你要写一个 WordPress 插件,需要获取当前页面的 URL。 你可能会这样写:

$current_url = $_SERVER['REQUEST_URI'];

看起来没问题,对吧? 但如果你的插件跑在一个 IIS 服务器上,而这个服务器并没有设置 REQUEST_URI 变量,你的代码就会报错,整个页面就崩溃了。

更糟糕的是,有些服务器可能会使用其他变量来表示 URL,比如 ORIG_PATH_INFOQUERY_STRING。 如果你的代码只认 REQUEST_URI,那在这些服务器上就彻底歇菜了。

所以,为了解决这些问题,WordPress 需要一个统一的入口,来获取当前页面的 URL,以及其他常用的服务器信息。 这个统一的入口,就是通过 wp_fix_server_vars() 来实现的。

二、 wp_fix_server_vars() 源码解析

废话不多说,让我们直接进入源码。 wp_fix_server_vars() 函数位于 wp-includes/functions.php 文件中。 以下是精简后的代码,去除了注释和一些不常用的分支:

function wp_fix_server_vars() {
    global $_SERVER;

    $default_server_values = array(
        'SERVER_NAME'         => '',
        'SERVER_PORT'         => '80',
        'REQUEST_URI'         => '',
        'PATH_INFO'           => '',
        'PATH_TRANSLATED'     => '',
        'SCRIPT_NAME'         => '',
        'SCRIPT_FILENAME'     => '',
        'PHP_SELF'            => '',
        'REMOTE_ADDR'         => '',
        'REMOTE_PORT'         => '',
        'SERVER_PROTOCOL'     => '',
        'QUERY_STRING'        => '',
        'HTTP_ACCEPT'         => '',
        'HTTP_USER_AGENT'     => '',
        'HTTP_ACCEPT_ENCODING' => '',
        'HTTP_ACCEPT_LANGUAGE' => '',
        'SERVER_SOFTWARE'     => '',
        'REMOTE_HOST'         => '',
    );

    // Merge with some empty default values.
    $_SERVER = array_merge( $default_server_values, $_SERVER );

    // Fix for IIS when running with PHP ISAPI.
    if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( $_SERVER['SERVER_SOFTWARE'], 'IIS' ) !== false ) {

        // IIS Mod-Rewrite.
        if ( ! isset( $_SERVER['REQUEST_URI'] ) || empty( $_SERVER['REQUEST_URI'] ) ) {
            $_SERVER['REQUEST_URI'] = wp_get_server_var( 'SCRIPT_NAME' );
            if ( isset( $_SERVER['QUERY_STRING'] ) && ! empty( $_SERVER['QUERY_STRING'] ) ) {
                $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

        // Handle PATH_INFO and PATH_TRANSLATED for IIS.
        if ( empty( $_SERVER['PATH_INFO'] ) && ! empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
            $_SERVER['PATH_INFO']    = $_SERVER['ORIG_PATH_INFO'];
        }

        if ( empty( $_SERVER['PATH_TRANSLATED'] ) && ! empty( $_SERVER['ORIG_PATH_TRANSLATED'] ) ) {
            $_SERVER['PATH_TRANSLATED'] = $_SERVER['ORIG_PATH_TRANSLATED'];
        }
    }

    // Fix for CGI scripts that set SCRIPT_FILENAME to something ending in .php only.  PHP oddness.
    if ( isset( $_SERVER['SCRIPT_FILENAME'] ) && ( false !== strpos( $_SERVER['SCRIPT_FILENAME'], '.php' ) ) && ( basename( $_SERVER['SCRIPT_FILENAME'] ) != basename( __FILE__ ) ) ) {
        $_SERVER['SCRIPT_FILENAME'] = realpath( __FILE__ );
    }

    // Fix for PHP as CGI hosts that set SCRIPT_NAME to a broken path.
    $script_name = wp_get_server_var( 'SCRIPT_NAME' );
    if ( isset( $script_name ) && ( basename( $script_name ) != basename( __FILE__ ) ) ) {
        $dirname = dirname( __FILE__ );
        if ( strpos( $script_name, $dirname ) === 0 ) {
            $_SERVER['SCRIPT_NAME'] = str_replace( $dirname, '', $script_name );
        }
    }

    // Fix for when PATH_INFO is not set but REQUEST_URI is set.
    $path_info = wp_get_server_var( 'PATH_INFO' );
    $request_uri = wp_get_server_var( 'REQUEST_URI' );
    if ( empty( $path_info ) && ! empty( $request_uri ) ) {
        $file_name = basename( wp_get_server_var( 'SCRIPT_FILENAME' ) );
        if ( strpos( $request_uri, $file_name ) !== false ) {
            $_SERVER['PATH_INFO'] = str_replace( $file_name, '', $request_uri );
        }
    }

    // Further sanitizing of PATH_INFO.
    if ( ! empty( $_SERVER['PATH_INFO'] ) ) {
        $_SERVER['PATH_INFO'] = str_replace( array( '?', '&' ), '', $_SERVER['PATH_INFO'] );
        $_SERVER['PATH_INFO'] = '/' . ltrim( $_SERVER['PATH_INFO'], '/' );
    }
}

/**
 * Retrieves a value from the $_SERVER array.
 *
 * @since 4.6.0
 *
 * @param string $key The index in the $_SERVER array.
 * @param mixed  $default Optional. Value to return if the key does not exist. Default null.
 * @return mixed The value in the $_SERVER array, or the default value if not set.
 */
function wp_get_server_var( $key, $default = null ) {
    return isset( $_SERVER[ $key ] ) ? $_SERVER[ $key ] : $default;
}

让我们一行一行地分析这段代码:

  1. global $_SERVER;: 声明 $_SERVER 为全局变量,以便在函数内部可以修改它。

  2. $default_server_values: 定义一个包含常用 $_SERVER 变量的数组,并设置了默认值。 如果服务器没有提供这些变量,WordPress 将使用这些默认值。

    变量名 默认值 描述
    SERVER_NAME '' 服务器主机名
    SERVER_PORT '80' 服务器端口号
    REQUEST_URI '' 请求的 URI
    PATH_INFO '' 包含关于请求路径的信息
    PATH_TRANSLATED '' 服务器将 PATH_INFO 映射到文件系统的路径
    SCRIPT_NAME '' 当前执行脚本的路径
    SCRIPT_FILENAME '' 当前执行脚本的完整路径
    PHP_SELF '' 当前脚本相对于根目录的路径
    REMOTE_ADDR '' 客户端 IP 地址
    REMOTE_PORT '' 客户端端口号
    SERVER_PROTOCOL '' 服务器使用的协议
    QUERY_STRING '' 查询字符串
    HTTP_ACCEPT '' 客户端可以接收的内容类型
    HTTP_USER_AGENT '' 客户端 User-Agent 字符串
    HTTP_ACCEPT_ENCODING '' 客户端支持的编码方式
    HTTP_ACCEPT_LANGUAGE '' 客户端支持的语言
    SERVER_SOFTWARE '' 服务器软件信息
    REMOTE_HOST '' 客户端主机名
  3. $_SERVER = array_merge( $default_server_values, $_SERVER );: 将 $default_server_values 数组和 $_SERVER 数组合并。 如果 $_SERVER 中没有某个变量,就使用 $default_server_values 中的默认值。 这样可以确保 $_SERVER 数组中至少包含这些常用的变量,即使服务器没有提供它们。

  4. if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( $_SERVER['SERVER_SOFTWARE'], 'IIS' ) !== false ) { ... }: 这是一个针对 IIS 服务器的特殊处理。 IIS 服务器在某些情况下,不会正确设置 REQUEST_URIPATH_INFOPATH_TRANSLATED 变量。 这段代码会尝试根据其他变量来推断这些值。

    • 如果 REQUEST_URI 为空,就使用 SCRIPT_NAMEQUERY_STRING 来构建它。
    • 如果 PATH_INFOPATH_TRANSLATED 为空,就使用 ORIG_PATH_INFOORIG_PATH_TRANSLATED 来填充它们。
  5. if ( isset( $_SERVER['SCRIPT_FILENAME'] ) && ( false !== strpos( $_SERVER['SCRIPT_FILENAME'], '.php' ) ) && ( basename( $_SERVER['SCRIPT_FILENAME'] ) != basename( __FILE__ ) ) { ... }: 针对某些 CGI 服务器的修复。 这些服务器可能会将 SCRIPT_FILENAME 设置为一个以 .php 结尾的路径,但这并不是真正的脚本文件。 这段代码会尝试将 SCRIPT_FILENAME 设置为当前脚本的真实路径。

  6. if ( isset( $script_name ) && ( basename( $script_name ) != basename( __FILE__ ) ) { ... }: 也是一个针对 CGI 服务器的修复。 有些 CGI 服务器会将 SCRIPT_NAME 设置为一个错误的路径。 这段代码会尝试修正 SCRIPT_NAME

  7. if ( empty( $path_info ) && ! empty( $request_uri ) ) { ... }: 如果 PATH_INFO 为空,但 REQUEST_URI 不为空,就尝试从 REQUEST_URI 中提取 PATH_INFO

  8. if ( ! empty( $_SERVER['PATH_INFO'] ) ) { ... }: 对 PATH_INFO 进行清理,移除 ?& 等字符,并确保以 / 开头。

  9. wp_get_server_var( $key, $default = null ): 这是一个辅助函数,用于从 $_SERVER 数组中获取值。 如果指定的 key 不存在,就返回指定的默认值。 这个函数可以避免直接访问 $_SERVER 数组时出现 "Undefined index" 错误。

三、 重点变量解析

wp_fix_server_vars() 主要关注以下几个变量:

  • REQUEST_URI: 请求的 URI,包括路径和查询字符串。 这是获取当前页面 URL 的关键变量。

  • PATH_INFO: 包含关于请求路径的额外信息。 通常用于 URL 重写。

  • SCRIPT_NAME: 当前执行脚本的路径。

  • SCRIPT_FILENAME: 当前执行脚本的完整路径。

这些变量在不同的服务器环境下,可能会有不同的值,甚至不存在。 wp_fix_server_vars() 的目标,就是确保这些变量在所有服务器环境下都可用,并且具有一致的含义。

四、 实际应用场景

那么,wp_fix_server_vars() 在 WordPress 中是如何使用的呢?

  1. wp() 函数: 在 WordPress 的主循环之前,wp() 函数会调用 wp_fix_server_vars(),以确保 $_SERVER 变量被正确初始化。

  2. get_site_url()get_bloginfo() 函数: 这些函数在获取站点 URL 和博客信息时,会依赖于 $_SERVER 变量。

  3. 插件和主题开发: 插件和主题开发者可以使用 wp_get_server_var() 函数来安全地访问 $_SERVER 变量,而不用担心 "Undefined index" 错误。

五、 总结

wp_fix_server_vars() 是一个不起眼,但至关重要的函数。 它通过标准化 $_SERVER 变量,确保 WordPress 在各种服务器环境下都能稳定运行。 虽然我们平时很少直接接触它,但它却默默地为我们解决了很多兼容性问题。

六、 最后的彩蛋

大家可能注意到了,wp_fix_server_vars() 函数中有很多针对 IIS 服务器的特殊处理。 这说明 IIS 服务器在某些方面确实比较特殊,需要特殊照顾。 所以,如果你正在使用 IIS 服务器,并且遇到了 WordPress 的问题,不妨检查一下 $_SERVER 变量是否正确设置。

好了,今天的“WordPress 源码扒皮”节目就到这里。 希望大家通过今天的学习,对 WordPress 的底层机制有了更深入的了解。 下次再见!

发表回复

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