阐述 WordPress `wp_check_filetype()` 函数的源码:如何通过文件头而非扩展名判断文件类型。

各位朋友,大家好!我是老码农,今天咱们来聊聊 WordPress 里面一个挺有意思的函数:wp_check_filetype(),重点是看看它怎么通过文件头(MIME Header)而不是简单的文件扩展名来判断文件类型。

一、文件类型判断的两种姿势:扩展名 vs. 文件头

在计算机世界里,要判断一个文件的类型,通常有两种办法:

  1. 看扩展名: 这是最简单粗暴的方法。比如 .jpg 结尾的文件,我们通常认为它是 JPEG 图片。但这方法有个致命缺点:扩展名是可以随便改的!你把一个 .txt 文件改成 .jpg,它仍然是文本文件,只是骗过了你的眼睛而已。

  2. 看文件头: 这种方法更靠谱。每个文件类型都有自己独特的“身份证”——文件头,也就是文件开头的一段特定字节。即使你改了扩展名,文件头还是不会变。wp_check_filetype() 函数就是利用这个特性来判断文件类型的。

二、wp_check_filetype() 函数:扒开源码看个透彻

wp_check_filetype() 函数位于 WordPress 的 wp-includes/functions.php 文件中。我们先来看看它的基本结构:

function wp_check_filetype( $filename, $mimes = null ) {
    /**
     * Filter the array of mime types used in wp_check_filetype().
     *
     * @since 2.0.0
     *
     * @param string[]|null $mimes Array of mime types keyed by their file extension regexes,
     *                             empty array, or null.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ) ? $mimes : get_allowed_mime_types() );

    $file = wp_check_filetype_and_ext( $filename, $mimes );

    return array(
        'ext'             => $file['ext'],
        'type'            => $file['type'],
        'proper_filename' => $file['proper_filename'],
    );
}

这个函数做了几件事:

  1. 获取允许的 MIME 类型: 首先,它通过 get_allowed_mime_types() 函数获取 WordPress 允许上传的文件类型列表。这个列表是一个关联数组,键是扩展名(正则表达式),值是 MIME 类型。同时,它还应用了 upload_mimes 过滤器,允许开发者自定义允许的文件类型。

  2. 调用 wp_check_filetype_and_ext() 函数: 接下来,它把文件名和 MIME 类型列表传给 wp_check_filetype_and_ext() 函数,这个函数才是真正干活的。

  3. 返回结果: 最后,它把 wp_check_filetype_and_ext() 函数返回的结果整理成一个数组,包含扩展名、MIME 类型和修正后的文件名。

三、wp_check_filetype_and_ext() 函数:核心逻辑揭秘

wp_check_filetype_and_ext() 函数位于 wp-includes/functions.php 文件中,是整个文件类型判断的核心。 让我们深入分析它:

function wp_check_filetype_and_ext( $filename, $mimes = null, $allowed_filesize = 0 ) {
    $proper_filename = $filename;

    /**
     * Filter the array of mime types used in wp_check_filetype_and_ext().
     *
     * @since 3.0.0
     *
     * @param string[]|null $mimes Array of mime types keyed by their file extension regexes,
     *                             empty array, or null.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ) ? $mimes : get_allowed_mime_types() );

    // Filesize check
    if ( $allowed_filesize > 0 && filesize( $filename ) > $allowed_filesize ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    $type = false;
    $ext  = false;

    foreach ( (array) $mimes as $ext_preg => $mime_match ) {
        $ext_preg = '!.(' . $ext_preg . ')$!i';
        if ( preg_match( $ext_preg, $filename, $ext_array ) ) {
            $type = $mime_match;
            $ext  = $ext_array[1];
            break;
        }
    }

    // If no match is found through extension, try mime sniffing.
    if ( false === $type ) {
        $file_data = wp_check_filetype_contents( $filename, $mimes );
        if ( false === $file_data['type'] ) {
            return array(
                'ext'             => false,
                'type'            => false,
                'proper_filename' => false,
            );
        }
        $ext  = $file_data['ext'];
        $type = $file_data['type'];

        // file name may have an invalid extension in it. (See issue #19513)
        $proper_filename = $file_data['proper_filename'];
    }

    return compact( 'ext', 'type', 'proper_filename' );
}

我们来逐步分析:

  1. 获取允许的 MIME 类型(再次):wp_check_filetype() 一样,它也首先获取允许的 MIME 类型列表,并且应用了 upload_mimes 过滤器。

  2. 文件大小检查: 如果设置了 $allowed_filesize 参数,则会检查文件大小是否超过限制。如果超过,直接返回 false

  3. 通过扩展名匹配: 它遍历允许的 MIME 类型列表,使用正则表达式匹配文件名后缀。如果找到了匹配的扩展名,就认为找到了文件类型,并设置 $type$ext 变量。

  4. MIME 嗅探(核心): 如果通过扩展名没有找到匹配的文件类型,那么就轮到 MIME 嗅探上场了!它会调用 wp_check_filetype_contents() 函数,通过读取文件内容(文件头)来判断文件类型。

  5. 返回结果: 最后,它把找到的扩展名、MIME 类型和修正后的文件名打包成一个数组返回。

四、wp_check_filetype_contents() 函数:MIME 嗅探的秘密

wp_check_filetype_contents() 函数位于 wp-includes/functions.php 文件中,是 MIME 嗅探的关键。

function wp_check_filetype_contents( $filename, $mimes = null ) {
    if ( ! function_exists( 'mime_content_type' ) && ! function_exists( 'finfo_open' ) ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    /**
     * Filter the array of mime types used in wp_check_filetype_contents().
     *
     * @since 3.0.0
     *
     * @param string[]|null $mimes Array of mime types keyed by their file extension regexes,
     *                             empty array, or null.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ) ? $mimes : get_allowed_mime_types() );

    $file = fopen( $filename, 'rb' );
    if ( ! $file ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }
    $byte = '';
    $found = false;
    $offset = 0;

    /**
     * Files included in WordPress
     * These files (in the wp-includes directory) are checked
     * for file headers. Prevents malicious code from being
     * uploaded as one of these files.
     */
    $allowed_loaded_files = array(
        'wp-config.php',
        'wp-settings.php',
        'wp-mail.php',
        'wp-includes/template-loader.php',
        'wp-includes/functions.php',
    );
    $filename_check = basename( $filename );

    foreach ( $allowed_loaded_files as $allowed_file ) {
        if ( $filename_check === basename( $allowed_file ) ) {
            $found = true;
            break;
        }
    }

    if ( $found ) {
        fclose( $file );
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    foreach ( (array) $mimes as $ext_preg => $mime_match ) {
        // Attempt to read some content from the file
        $byte = fread( $file, 4096 ); // Read up to 4KB of the file
        if ( empty( $byte ) ) {
            continue;
        }

        // First we check for unicode Byte Order Marks (BOM)
        if ( 0 === strpos( $byte, "xEFxBBxBF" ) ) {
            $byte = substr( $byte, 3 );
        }

        if ( 0 === strpos( $byte, "xFFxFE" ) ) {
            $byte = substr( $byte, 2 );
        }

        if ( 0 === strpos( $byte, "xFExFF" ) ) {
            $byte = substr( $byte, 2 );
        }

        // Check for common file headers
        $signature = '';
        switch ( $mime_match ) {
            case 'image/jpeg':
                $signature = "xFFxD8xFF";
                break;
            case 'image/gif':
                $signature = "x47x49x46x38"; // GIF8
                break;
            case 'image/png':
                $signature = "x89PNGx0dx0ax1ax0a";
                break;
            case 'application/pdf':
                $signature = '%PDF-';
                break;
            // Add more cases for other MIME types as needed
        }

        if ( ! empty( $signature ) && 0 === strpos( $byte, $signature ) ) {
            $ext = str_replace( '!.(', '', $ext_preg ); // Remove the regex characters
            $ext = str_replace( ')$!i', '', $ext );
            $ext = explode( '|', $ext );
            $ext = $ext[0]; // Use the first extension if multiple are provided

            fclose( $file );
            return array(
                'ext'             => $ext,
                'type'            => $mime_match,
                'proper_filename' => $filename,
            );
        }

        rewind( $file );
    }

    fclose( $file );
    return array(
        'ext'             => false,
        'type'            => false,
        'proper_filename' => false,
    );
}

这部分代码稍微复杂一些,我们分解开来看:

  1. 检查扩展是否可用: 首先,它检查 mime_content_type 函数(已弃用,不推荐使用)和 finfo_open 函数是否存在。这两个函数都可以用来进行 MIME 嗅探,但是 WordPress 并没有使用它们。 如果这两个函数都不存在,就直接返回 false,表示无法进行 MIME 嗅探。

  2. 获取允许的 MIME 类型(再次): 和前面一样,获取允许的 MIME 类型列表,并应用过滤器。

  3. 打开文件: 使用 fopen() 函数以二进制读取模式打开文件。如果打开失败,直接返回 false

  4. 安全检查: 对文件名进行安全检查,防止恶意代码伪装成 WordPress 核心文件上传。如果文件名和 WordPress 核心文件相同,则返回 false

  5. 读取文件内容: 使用 fread() 函数读取文件的前 4KB 内容。读取这么多的目的是为了覆盖大部分文件类型的头部信息。

  6. 处理 Unicode BOM: 检查文件是否包含 Unicode 字节顺序标记(BOM)。如果包含,则移除 BOM。

  7. 匹配文件头: 遍历允许的 MIME 类型列表,检查文件头是否匹配已知的 MIME 类型签名。这里使用了 strpos() 函数来判断文件头是否以特定的字节序列开头。

    • 常见文件类型的签名:

      MIME 类型 签名
      image/jpeg xFFxD8xFF
      image/gif x47x49x46x38 (GIF8)
      image/png x89PNGx0dx0ax1ax0a
      application/pdf %PDF-
  8. 找到匹配项: 如果找到了匹配的文件头,就认为找到了文件类型,提取扩展名,关闭文件,然后返回包含扩展名、MIME 类型和修正后的文件名的数组。

  9. 未找到匹配项: 如果遍历完所有的 MIME 类型都没有找到匹配的文件头,就关闭文件,然后返回 false

五、代码示例:模拟 wp_check_filetype_contents() 的文件头判断

为了更好地理解 MIME 嗅探的原理,我们可以自己写一个简单的函数来模拟 wp_check_filetype_contents() 的文件头判断过程:

<?php

function my_check_file_header( $filename ) {
    $file = fopen( $filename, 'rb' );
    if ( ! $file ) {
        return false;
    }

    $header = fread( $file, 8 ); // 读取前8个字节
    fclose( $file );

    $mime_types = [
        'image/jpeg' => "xFFxD8xFF",
        'image/png'  => "x89PNGx0dx0ax1ax0a",
        'image/gif'  => "x47x49x46x38",
    ];

    foreach ( $mime_types as $mime_type => $signature ) {
        if ( substr( $header, 0, strlen( $signature ) ) === $signature ) {
            return $mime_type;
        }
    }

    return false;
}

// 示例用法
$filename = 'test.jpg'; // 修改为你的文件名
$mime_type = my_check_file_header( $filename );

if ( $mime_type ) {
    echo "文件类型是: " . $mime_type . "n";
} else {
    echo "无法确定文件类型。n";
}

?>

这个例子只包含了 JPEG、PNG 和 GIF 三种文件类型的判断,你可以根据需要添加更多的 MIME 类型。

六、总结与思考

  • wp_check_filetype() 函数是 WordPress 中用于判断文件类型的重要工具。
  • 它首先尝试通过扩展名来判断文件类型,如果失败,则使用 MIME 嗅探技术,通过读取文件头来判断文件类型。
  • MIME 嗅探技术可以提高文件类型判断的准确性,防止恶意文件被上传。
  • wp_check_filetype_contents() 函数是 MIME 嗅探的核心,它读取文件内容,然后与已知的 MIME 类型签名进行匹配。

思考题:

  1. MIME 嗅探技术有哪些局限性?例如,对于某些文件类型,文件头可能不够明确,或者容易被伪造。
  2. 除了文件头,还有哪些方法可以用来判断文件类型?例如,可以使用第三方库来分析文件内容,或者使用机器学习模型来进行分类。
  3. 如何在 WordPress 中自定义允许上传的文件类型?你可以使用 upload_mimes 过滤器来实现。

好了,今天的讲座就到这里。希望大家通过这次学习,对 WordPress 的文件类型判断机制有了更深入的了解。记住,编程的世界充满了乐趣,只要你肯探索,就能发现更多的秘密!下次再见!

发表回复

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