各位观众老爷们,大家好!今天咱们来聊聊 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()
的源码,看看它是如何“看内在”的。 为了方便讲解,我们将代码分解成几个部分:
- 初始化和参数处理
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
和$ext
为false
,稍后会根据文件内容进行更新。 - 如果定义了
WP_MIME_TYPES
常量,并且其值为 true,则使用get_allowed_mime_types()
获取MIME类型,并覆盖$mimes
变量。
- 通过文件名获取扩展名
$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
。
- 通过扩展名匹配 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
变量,并跳出循环。
- 通过文件内容确定 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 类型。 这是最可靠的方法,因为它直接读取文件内容,并根据预定义的“魔法数字”数据库来判断文件类型。
- 处理文件名中的 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) |
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()
函数。
咱们下期再见!