深入理解 `wp_check_filetype()` 函数的源码,它是如何通过文件内容而不是扩展名来判断文件类型的?

各位观众老爷们,大家好!今天咱们来聊聊 WordPress 里的一个神奇函数:wp_check_filetype()。别看它名字平平无奇,但它可是文件类型识别界的幕后英雄,尤其擅长“看内在”,而不是只看“外表”(也就是文件扩展名)。

咱们先来明确一下问题: 为什么我们需要通过文件内容来判断文件类型? 扩展名不是已经告诉我们了吗? 答案是: 扩展名是靠不住的! 任何人都可以随意修改扩展名,把一个恶意 PHP 脚本伪装成一张人畜无害的 JPG 图片。所以,为了安全起见,我们需要更可靠的方法来验证文件类型。

wp_check_filetype() 的主要任务就是:在扩展名不可靠的情况下,通过读取文件内容(通常是文件头几个字节)来判断文件类型,并返回一个包含文件扩展名和 MIME 类型的数组。

一、wp_check_filetype() 的基本用法

首先,我们看看 wp_check_filetype() 的基本用法。它接收三个参数:

/**
 * Retrieve file type based on file name and content.
 *
 * @since 2.0.0
 *
 * @param string      $filename File name or path.
 * @param string|null $mimes    Optional. Array of mime types keyed by their file extension regexps.
 * @param string|null $allowed_filesize Optional. Allowed file size in bytes.
 * @return array Values for 'ext', 'type', and 'proper_filename'.
 */
function wp_check_filetype( $filename, $mimes = null, $allowed_filesize = null ) {
    // Function body will be explained later
}
  • $filename: 文件名或文件路径。
  • $mimes: (可选) MIME 类型数组,键是文件扩展名的正则表达式,值是 MIME 类型。如果为空,则使用 WordPress 默认的 MIME 类型数组。
  • $allowed_filesize: (可选) 允许的文件大小,字节为单位。如果文件超过指定大小,则返回空数组。

返回值是一个数组,包含以下三个元素:

  • ext: 文件扩展名 (小写)。
  • type: MIME 类型。
  • proper_filename: 处理后的文件名,主要用于解决 Unicode 字符问题。

简单示例:

$file = 'path/to/my_image.jpg';
$filetype = wp_check_filetype( $file );

if ( ! empty( $filetype['ext'] ) ) {
    echo '文件扩展名: ' . $filetype['ext'] . '<br>';
    echo 'MIME 类型: ' . $filetype['type'] . '<br>';
} else {
    echo '无法确定文件类型。';
}

二、wp_check_filetype() 的源码剖析

接下来,我们深入研究 wp_check_filetype() 的源码,看看它是如何“看内在”的。 为了方便讲解,我们将代码分解成几个部分:

  1. 初始化和参数处理
function wp_check_filetype( $filename, $mimes = null, $allowed_filesize = null ) {
    $proper_filename = '';

    /**
     * Filters the list of mime types used by wp_check_filetype().
     *
     * @since 2.0.0
     *
     * @param string[]|null $mimes Array of mime types keyed by their file extension regexps.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ? $mimes : get_allowed_mime_types() ) );

    // Check filesize, if needed.
    if ( ! empty( $allowed_filesize ) ) {
        $filesize = filesize( $filename );
        if ( $filesize > $allowed_filesize ) {
            return array(); // File is too large.
        }
    }

    $type = false;
    $ext  = false;

    // Use MIME types defined in wp-config.php?
    if ( defined( 'WP_MIME_TYPES' ) && WP_MIME_TYPES ) {
        $wp_mimes = get_allowed_mime_types();
        if ( is_array( $wp_mimes ) ) {
            $mimes = $wp_mimes;
        }
    }
  • 首先,初始化 $proper_filename 为空字符串。
  • 然后,通过 apply_filters( 'upload_mimes', ... ) 钩子,允许开发者自定义 MIME 类型数组。 如果 $mimes 参数为空,则使用 get_allowed_mime_types() 函数获取 WordPress 默认的 MIME 类型数组。这个钩子非常重要,因为它允许你添加或修改 WordPress 支持的文件类型。
  • 如果提供了 $allowed_filesize 参数,则检查文件大小。 如果文件超过允许的大小,则直接返回一个空数组,表示无法确定文件类型。
  • 初始化 $type$extfalse,稍后会根据文件内容进行更新。
  • 如果定义了 WP_MIME_TYPES 常量,并且其值为 true,则使用 get_allowed_mime_types() 获取MIME类型,并覆盖 $mimes 变量。
  1. 通过文件名获取扩展名
    $file_parts = pathinfo( $filename );
    $ext        = isset( $file_parts['extension'] ) ? strtolower( $file_parts['extension'] ) : '';

    // Check to see if file extension is blocked
    if ( wp_is_file_blocked( $filename, $mimes ) ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    // If extension is blocked, return false.
    if ( ! empty( $ext ) && ! preg_match( '/^[a-zA-Z0-9]+$/', $ext ) ) {
        $ext = false;
    }
  • 使用 pathinfo() 函数解析文件名,获取文件扩展名。
  • 使用 wp_is_file_blocked() 检查文件名是否被阻止上传。如果被阻止,则直接返回包含三个 false 值的数组。
  • 验证扩展名是否只包含字母和数字。如果包含其他字符,则认为扩展名无效,并将其设置为 false
  1. 通过扩展名匹配 MIME 类型
    if ( $mimes && ! empty( $ext ) ) {
        foreach ( $mimes as $ext_preg => $mime_match ) {
            $ext_preg = '!.(' . $ext_preg . ')$!i';
            if ( preg_match( $ext_preg, $filename, $matches ) ) {
                $type = $mime_match;
                break;
            }
        }
    }
  • 如果提供了 $mimes 数组,并且文件扩展名存在,则遍历 $mimes 数组,尝试通过扩展名匹配 MIME 类型。
  • 对于每个扩展名正则表达式,使用 preg_match() 函数进行匹配。如果匹配成功,则将对应的 MIME 类型赋值给 $type 变量,并跳出循环。
  1. 通过文件内容确定 MIME 类型
    if ( ! $type && function_exists( 'wp_get_mime_types' ) ) {
        $mime_types = wp_get_mime_types();
        foreach ( $mime_types as $key => $value ) {
            if ( strpos( $key, $ext ) !== false ) {
                $type = $value;
                break;
            }
        }
    }

    if ( ( ! $type || false === strpos( $type, '/' ) ) && function_exists( 'mime_content_type' ) && @ini_get( 'fileinfo.magic_file' ) ) {
        $mime = @mime_content_type( $filename );
        if ( $mime && false !== strpos( $mime, '/' ) ) {
            $type = $mime;
        }
    } else {
        // Use PHP fileinfo extension
        if ( function_exists( 'finfo_open' ) ) {
            $finfo = finfo_open( FILEINFO_MIME_TYPE );
            if ( $finfo ) {
                $mime = finfo_file( $finfo, $filename );
                finfo_close( $finfo );

                if ( $mime ) {
                    $type = $mime;
                }
            }
        }
    }
  • 如果仍然无法确定 MIME 类型,并且 wp_get_mime_types 函数存在,则使用该函数来查找MIME类型。
  • 如果仍然无法确定 MIME 类型,并且 mime_content_type() 函数存在 (并且 fileinfo.magic_file 配置项已设置),则调用 mime_content_type() 函数,通过读取文件内容来确定 MIME 类型。注意,这里使用了 @ 符号来抑制可能出现的错误。
  • 如果 finfo_open() 函数存在 (PHP fileinfo 扩展),则使用该扩展来确定 MIME 类型。 这是最可靠的方法,因为它直接读取文件内容,并根据预定义的“魔法数字”数据库来判断文件类型。
  1. 处理文件名中的 Unicode 字符
    /**
     * Filters the "proper" name of the file.
     *
     * @since 2.1.0
     *
     * @param string $proper_filename The proper name of the file.
     * @param string $filename        The name of the file.
     */
    $proper_filename = apply_filters( 'sanitize_file_name', wp_basename( $filename ), $filename );
    $proper_filename = str_replace( ' ', '-', $proper_filename );
    $proper_filename = preg_replace( '/[^A-Za-z0-9-.]+/i', '', $proper_filename );
    $proper_filename = trim( $proper_filename, '.-' );

    return compact( 'ext', 'type', 'proper_filename' );
}
  • 使用 apply_filters( 'sanitize_file_name', ... ) 钩子,允许开发者自定义文件名的清理过程。
  • 使用 wp_basename() 函数获取文件名(不包含路径)。
  • 将文件名中的空格替换为短横线。
  • 移除文件名中所有非字母、数字、短横线和句点的字符。
  • 移除文件名开头和结尾的短横线和句点。
  • 最后,使用 compact() 函数创建一个包含 ext, type, 和 proper_filename 三个元素的数组,并返回。

三、wp_check_filetype() 如何通过文件内容判断文件类型?

关键就在于上面源码中的 mime_content_type() 函数 和 finfo 扩展。 这两个家伙都依赖于一个叫做 "magic numbers" 的概念。

  • 什么是 "magic numbers"?

    "magic numbers" 是一些文件类型特有的、位于文件起始位置的几个字节。 比如,JPEG 文件的 magic number 通常是 FF D8 FF E0,PNG 文件的 magic number 是 89 50 4E 47 0D 0A 1A 0A

  • mime_content_type() 的工作原理

    mime_content_type() 函数会读取文件的前几个字节,然后在一个预定义的 "magic number" 数据库中查找匹配的记录。如果找到匹配的记录,就返回对应的 MIME 类型。 这个数据库通常是一个名为 magic.mime 的文件,位于系统的某个目录下。

  • finfo 扩展的工作原理

    finfo 扩展比 mime_content_type() 更强大、更灵活。 它也使用 "magic numbers" 数据库,但它提供了更多的配置选项,并且可以识别更多的文件类型。 使用 finfo 扩展时,你需要先调用 finfo_open() 函数创建一个 finfo 对象,然后调用 finfo_file() 函数来分析文件,最后调用 finfo_close() 函数关闭 finfo 对象。

为了更直观的展示不同文件类型对应的 magic number,可以参考下表:

文件类型 扩展名 Magic Number (十六进制)
JPEG .jpg, .jpeg FF D8 FF E0 (或者 FF D8 FF E1)
PNG .png 89 50 4E 47 0D 0A 1A 0A
GIF .gif 47 49 46 38 37 61 (GIF87a) 或者 47 49 46 38 39 61 (GIF89a)
PDF .pdf 25 50 44 46 ( %PDF )
ZIP .zip 50 4B 03 04 (PKx03x04)
Microsoft Word (旧版 .doc) .doc D0 CF 11 E0 A1 B1 1A E1
Microsoft Word (新版 .docx) .docx 50 4B 03 04 14 00 06 00
MP3 .mp3 通常没有固定的 magic number, 但可能包含 ID3 标签 (49 44 33) 在文件开头

四、自定义 MIME 类型

正如前面提到的,wp_check_filetype() 函数使用了 apply_filters( 'upload_mimes', ... ) 钩子,允许你自定义 MIME 类型数组。 这非常有用,可以让你支持 WordPress 默认不支持的文件类型。

例如,如果你想支持 .svg 文件,可以这样操作:

function my_custom_mime_types( $mimes ) {
    $mimes['svg'] = 'image/svg+xml';
    return $mimes;
}
add_filter( 'upload_mimes', 'my_custom_mime_types' );

这段代码将 .svg 扩展名关联到 image/svg+xml MIME 类型。 之后,你就可以上传 .svg 文件了。

五、安全注意事项

虽然 wp_check_filetype() 函数可以帮助我们识别文件类型,但它并不能完全保证安全性。 恶意用户仍然可能通过各种手段绕过检测,上传恶意文件。 因此,在处理用户上传的文件时,务必采取以下安全措施:

  • 不要信任用户上传的文件名。 始终对文件名进行清理和验证。
  • 限制上传文件的类型。 只允许上传必要的文件类型。
  • 将用户上传的文件存储在非 Web 可访问的目录中。 避免直接通过 URL 访问用户上传的文件。
  • 对用户上传的文件进行扫描。 使用病毒扫描软件或其他安全工具来检测恶意代码。

六、总结

wp_check_filetype() 函数是 WordPress 文件类型识别的重要组成部分。 它通过文件内容(主要是 magic numbers)来判断文件类型,从而提高了安全性。 但是,它并不是万能的,仍然需要配合其他安全措施来保护你的网站。

总而言之,理解 wp_check_filetype() 函数的源码,不仅可以帮助你更好地理解 WordPress 的内部机制,还可以让你更安全地处理用户上传的文件。 希望今天的讲座对你有所帮助!

最后,留个小作业: 阅读 wp_is_file_blocked() 函数的源码,看看它是如何判断文件是否被阻止上传的。 提示:它主要依赖于 get_allowed_mime_types() 函数。

咱们下期再见!

发表回复

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