探究 WordPress `wp_remote_fopen()` 函数源码:它如何通过 `WP_Http` 类模拟 `fopen()`。

呦吼!各位代码大佬、WordPress 爱好者们,晚上好!今天咱们来聊聊 WordPress 里的一个“小秘密”—— wp_remote_fopen() 函数,以及它背后的大功臣 WP_Http 类。

想象一下,你要在一个 WordPress 插件或者主题里,从远程服务器读取一个文件,比如读取一个 JSON 数据,或者获取一个最新的版本号。你可能会想到用 PHP 原生的 fopen() 函数。但是,且慢!WordPress 团队告诉你,fopen() 可能会有一些安全隐患,而且有些服务器可能禁用了 allow_url_fopen 选项,导致你的代码无法正常工作。

所以,WordPress 提供了一个更安全、更可靠的替代方案—— wp_remote_fopen()。 它实际上是对 WP_Http 类的一个巧妙封装,模拟了 fopen() 的功能,但又避开了 fopen() 的一些坑。

让我们一起扒开它的源码,看看它是怎么工作的吧!

第一部分: wp_remote_fopen() 的庐山真面目

首先,我们来看看 wp_remote_fopen() 函数的定义(位于 /wp-includes/functions.php 文件中)。

function wp_remote_fopen( $url, $context = false ) {
    if ( false === stripos( $url, 'http://' ) && false === stripos( $url, 'https://' ) ) {
        return false;
    }

    $options = array( 'sslverify' => false );
    if ( $context ) {
        $options['stream'] = $context;
    }

    $response = wp_remote_get( $url, $options );

    if ( is_wp_error( $response ) ) {
        return false;
    }

    if ( 200 != wp_remote_retrieve_response_code( $response ) ) {
        return false;
    }

    return wp_remote_retrieve_body( $response );
}

是不是感觉很简单?其实它主要做了以下几件事:

  1. 安全检查: 确保 URL 是 HTTP 或 HTTPS 协议,防止读取本地文件或其他恶意 URL。
  2. 配置选项: 创建一个选项数组,默认禁用 SSL 验证(sslverify => false)。如果你需要传递 stream context,也会在这里进行设置。
  3. 发起请求: 使用 wp_remote_get() 函数发起一个 GET 请求,获取远程数据。
  4. 错误处理: 检查 wp_remote_get() 是否返回错误,以及 HTTP 状态码是否为 200。如果出错,返回 false
  5. 返回数据: 如果一切顺利,使用 wp_remote_retrieve_body() 函数提取响应体,并返回。

可以看到,wp_remote_fopen() 并没有直接使用 fopen(),而是借助了 wp_remote_get()WP_Http 类来完成远程数据读取的任务。

第二部分:WP_Http 类的内部乾坤

WP_Http 类(位于 /wp-includes/class-http.php 文件中)才是整个远程请求的核心。它负责处理各种 HTTP 请求方法(GET、POST、PUT 等),设置请求头,发送数据,接收响应,以及处理各种网络错误。

WP_Http 类是一个抽象类,它定义了一系列抽象方法,需要由具体的 HTTP 传输类来实现。 WordPress 默认提供了几个传输类,比如 WP_Http_Curl (如果服务器支持 cURL 扩展), WP_Http_Streams (使用 PHP 的 streams 功能), 和 WP_Http_Fsockopen (使用 fsockopen 函数)。

我们先来看看 WP_Http 类的一些核心方法:

  • request( $url, $args = array() ): 发起 HTTP 请求的总入口。 它会根据 $args 参数,选择合适的 HTTP 传输类,并调用其 request() 方法。
  • get( $url, $args = array() ): 发起 GET 请求的快捷方法。 实际上是调用 request() 方法,并将 method 设置为 ‘GET’。
  • post( $url, $args = array() ): 发起 POST 请求的快捷方法。 实际上是调用 request() 方法,并将 method 设置为 ‘POST’。
  • head( $url, $args = array() ): 发起 HEAD 请求的快捷方法。
  • get_response_headers( $response ): 从响应中提取 HTTP 头部信息。
  • get_response_body( $response ): 从响应中提取 HTTP 响应体。

再来看一下 request 方法是如何工作的(简化版):

abstract class WP_Http {

    public function request( $url, $args = array() ) {
        $args = wp_parse_args( $args );

        $transports = array(
            'WP_Http_Curl',
            'WP_Http_Streams',
            'WP_Http_Fsockopen',
        );

        // Try transports until we get one that works.
        foreach ( $transports as $class ) {
            $ret = false;
            if ( ! class_exists( $class ) ) {
                continue;
            }

            $transport = new $class();

            if ( ! $transport->test( $url ) ) {
                continue;
            }

            $ret = $transport->request( $url, $args );

            if ( ! is_wp_error( $ret ) ) {
                return $ret;
            }
        }

        return new WP_Error( 'http_request_failed', __( 'An HTTP error occurred.' ) );
    }
}

这个方法的核心逻辑是:

  1. 参数解析: 使用 wp_parse_args() 函数将 $args 参数合并到默认参数中。
  2. 传输类选择: 遍历 transports 数组,尝试使用不同的 HTTP 传输类。
  3. 传输类测试: 调用传输类的 test() 方法,判断当前环境是否支持该传输类。
  4. 发起请求: 如果支持,调用传输类的 request() 方法发起请求。
  5. 错误处理: 如果请求成功,返回响应;否则,继续尝试下一个传输类。如果所有传输类都失败,返回一个 WP_Error 对象。

第三部分:HTTP 传输类的内部实现

我们以 WP_Http_Curl 类为例,看看它是如何使用 cURL 扩展来发起 HTTP 请求的。

class WP_Http_Curl extends WP_Http {

    /**
     * Send the request
     *
     * @param string $url URL to retrieve
     * @param array $args Optional. Override the defaults.
     * @return array An array containing 'headers', 'body', 'response', 'cookies', 'filename'
     */
    public function request( $url, $args = array() ) {
        $defaults = array(
            'method'      => 'GET',
            'timeout'     => 5,
            'redirection' => 5,
            'httpversion' => '1.0',
            'user-agent'  => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url(),
            'reject_unsafe_urls' => false,
            'blocking'    => true,
            'headers'     => array(),
            'cookies'     => array(),
            'body'        => null,
            'compress'    => false,
            'decompress'  => true,
            'sslverify'   => true,
            'stream'      => false,
            'filename'    => null,
            'limit_response_size' => null,
        );

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

        $curl = curl_init( $url );

        $http_headers = WP_Http::normalize_headers( $args['headers'] );
        $http_headers['Expect'] = '';

        $headers = array();
        foreach ( $http_headers as $name => $value ) {
            $headers[] = $name . ': ' . $value;
        }

        curl_setopt( $curl, CURLOPT_HTTPHEADER, $headers );
        curl_setopt( $curl, CURLOPT_URL, $url );
        curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $curl, CURLOPT_HEADER, true );
        curl_setopt( $curl, CURLOPT_CONNECTTIMEOUT, $args['timeout'] );
        curl_setopt( $curl, CURLOPT_TIMEOUT, $args['timeout'] );
        curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, $args['sslverify'] );
        curl_setopt( $curl, CURLOPT_USERAGENT, $args['user-agent'] );

        // Set the request method.
        switch ( strtoupper( $args['method'] ) ) {
            case 'HEAD':
                curl_setopt( $curl, CURLOPT_NOBODY, true );
                break;
            case 'POST':
                curl_setopt( $curl, CURLOPT_POST, true );
                curl_setopt( $curl, CURLOPT_POSTFIELDS, $args['body'] );
                break;
            case 'PUT':
                curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, 'PUT' );
                curl_setopt( $curl, CURLOPT_POSTFIELDS, $args['body'] );
                break;
            default:
                curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, strtoupper( $args['method'] ) );
                break;
        }

        $response = curl_exec( $curl );

        if ( curl_errno( $curl ) ) {
            $error_message = curl_error( $curl );
            curl_close( $curl );
            return new WP_Error( 'curl_error', $error_message );
        }

        $header_size = curl_getinfo( $curl, CURLINFO_HEADER_SIZE );
        $header = substr( $response, 0, $header_size );
        $body = substr( $response, $header_size );
        $status_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE );

        curl_close( $curl );

        $response_headers = WP_Http::processHeaders( $header );

        $returned_response = array(
            'headers'  => $response_headers,
            'body'     => $body,
            'response' => array( 'code' => $status_code, 'message' => '' ),
            'cookies'  => array(), // TODO: implement cookie handling
        );

        return $returned_response;
    }

    /**
     * Test if cURL is installed and enabled.
     *
     * @param string $url Not used.
     * @return bool True if cURL is installed and enabled.
     */
    public function test( $url = '' ) {
        return function_exists( 'curl_init' ) && function_exists( 'curl_exec' );
    }
}

这个方法的主要步骤是:

  1. 参数解析:WP_Http::request() 方法一样,先解析参数。
  2. 初始化 cURL: 使用 curl_init() 函数初始化一个 cURL 句柄。
  3. 设置 cURL 选项: 设置各种 cURL 选项,比如 URL、HTTP 头部、超时时间、SSL 验证、User-Agent 等。
  4. 设置请求方法: 根据 $args['method'] 参数,设置请求方法(GET、POST、PUT 等)。
  5. 执行请求: 使用 curl_exec() 函数执行请求,获取响应。
  6. 错误处理: 检查 curl_errno() 函数的返回值,判断是否发生错误。
  7. 提取响应: 从响应中提取 HTTP 头部和响应体。
  8. 返回结果: 将响应信息封装成一个数组,并返回。

WP_Http_StreamsWP_Http_Fsockopen 类的实现方式类似,只是使用了不同的 PHP 函数来发起 HTTP 请求。

*第四部分: `wpremote` 系列函数**

除了 wp_remote_fopen() 之外,WordPress 还提供了一系列 wp_remote_* 函数,它们都是对 WP_Http 类的一个封装,用于发起不同类型的 HTTP 请求。

函数名 描述 对应 HTTP 方法
wp_remote_get() 发起一个 GET 请求,获取远程数据。 GET
wp_remote_post() 发起一个 POST 请求,向远程服务器提交数据。 POST
wp_remote_head() 发起一个 HEAD 请求,获取远程服务器的 HTTP 头部信息。 HEAD
wp_remote_request() 发起一个 HTTP 请求,可以自定义请求方法、头部、body 等参数。 这是最通用的一个函数,其他 wp_remote_* 函数都是基于它实现的。 任意
wp_remote_retrieve_body() wp_remote_* 函数返回的响应中,提取 HTTP 响应体。 N/A
wp_remote_retrieve_response_code() wp_remote_* 函数返回的响应中,提取 HTTP 状态码。 N/A
wp_remote_retrieve_response_message() wp_remote_* 函数返回的响应中,提取 HTTP 状态信息。 N/A
wp_remote_retrieve_headers() wp_remote_* 函数返回的响应中,提取 HTTP 头部信息。 N/A

这些函数的使用方法都比较简单,可以参考 WordPress 官方文档。

第五部分:实战演练

让我们来看一个实际的例子,使用 wp_remote_get() 函数从远程服务器读取一个 JSON 数据。

$url = 'https://api.example.com/data.json'; // 替换成你的 API 地址

$response = wp_remote_get( $url );

if ( is_wp_error( $response ) ) {
    $error_message = $response->get_error_message();
    echo "Something went wrong: $error_message";
} else {
    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body );

    if ( $data ) {
        // 处理 JSON 数据
        foreach ( $data as $item ) {
            echo $item->name . '<br>';
        }
    } else {
        echo "Failed to decode JSON data.";
    }
}

这段代码做了以下几件事:

  1. 发起 GET 请求: 使用 wp_remote_get() 函数从指定的 URL 获取数据。
  2. 错误处理: 检查是否返回错误,如果出错,输出错误信息。
  3. 提取数据: 从响应中提取 HTTP 响应体。
  4. 解析 JSON: 使用 json_decode() 函数将 JSON 数据解析成 PHP 对象。
  5. 处理数据: 遍历 JSON 数据,并输出每个条目的 name 属性。

第六部分:总结与思考

通过今天的学习,我们深入了解了 wp_remote_fopen() 函数以及 WP_Http 类的内部实现。 我们可以看到,WordPress 团队为了保证代码的安全性和可靠性,做了很多努力。 WP_Http 类提供了一个统一的 HTTP 请求接口,可以方便地使用不同的 HTTP 传输类,并处理各种网络错误。

wp_remote_fopen() 函数只是 WP_Http 类的一个应用场景。 在实际开发中,我们可以根据自己的需求,选择合适的 wp_remote_* 函数,或者直接使用 WP_Http 类,来发起各种 HTTP 请求。

思考题:

  1. WP_Http 类为什么要设计成抽象类? 这样做有什么好处?
  2. WP_Http 类是如何处理 Cookie 的? (提示: 可以查看 WP_Http_Curl 类的源码,虽然代码中有一个 // TODO: implement cookie handling 注释,但可以了解大致思路。)
  3. 在什么情况下,你需要手动设置 WP_Http 类的 $args 参数?

希望今天的讲座对你有所帮助! 记住,代码的世界充满了乐趣,只要你肯钻研,就能发现更多的惊喜! 咱们下次再见!

发表回复

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