各位朋友,大家好!我是老码农,今天咱们来聊聊 WordPress 里面一个挺有意思的函数:wp_check_filetype()
,重点是看看它怎么通过文件头(MIME Header)而不是简单的文件扩展名来判断文件类型。
一、文件类型判断的两种姿势:扩展名 vs. 文件头
在计算机世界里,要判断一个文件的类型,通常有两种办法:
-
看扩展名: 这是最简单粗暴的方法。比如
.jpg
结尾的文件,我们通常认为它是 JPEG 图片。但这方法有个致命缺点:扩展名是可以随便改的!你把一个.txt
文件改成.jpg
,它仍然是文本文件,只是骗过了你的眼睛而已。 -
看文件头: 这种方法更靠谱。每个文件类型都有自己独特的“身份证”——文件头,也就是文件开头的一段特定字节。即使你改了扩展名,文件头还是不会变。
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'],
);
}
这个函数做了几件事:
-
获取允许的 MIME 类型: 首先,它通过
get_allowed_mime_types()
函数获取 WordPress 允许上传的文件类型列表。这个列表是一个关联数组,键是扩展名(正则表达式),值是 MIME 类型。同时,它还应用了upload_mimes
过滤器,允许开发者自定义允许的文件类型。 -
调用
wp_check_filetype_and_ext()
函数: 接下来,它把文件名和 MIME 类型列表传给wp_check_filetype_and_ext()
函数,这个函数才是真正干活的。 -
返回结果: 最后,它把
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' );
}
我们来逐步分析:
-
获取允许的 MIME 类型(再次): 和
wp_check_filetype()
一样,它也首先获取允许的 MIME 类型列表,并且应用了upload_mimes
过滤器。 -
文件大小检查: 如果设置了
$allowed_filesize
参数,则会检查文件大小是否超过限制。如果超过,直接返回false
。 -
通过扩展名匹配: 它遍历允许的 MIME 类型列表,使用正则表达式匹配文件名后缀。如果找到了匹配的扩展名,就认为找到了文件类型,并设置
$type
和$ext
变量。 -
MIME 嗅探(核心): 如果通过扩展名没有找到匹配的文件类型,那么就轮到 MIME 嗅探上场了!它会调用
wp_check_filetype_contents()
函数,通过读取文件内容(文件头)来判断文件类型。 -
返回结果: 最后,它把找到的扩展名、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,
);
}
这部分代码稍微复杂一些,我们分解开来看:
-
检查扩展是否可用: 首先,它检查
mime_content_type
函数(已弃用,不推荐使用)和finfo_open
函数是否存在。这两个函数都可以用来进行 MIME 嗅探,但是 WordPress 并没有使用它们。 如果这两个函数都不存在,就直接返回false
,表示无法进行 MIME 嗅探。 -
获取允许的 MIME 类型(再次): 和前面一样,获取允许的 MIME 类型列表,并应用过滤器。
-
打开文件: 使用
fopen()
函数以二进制读取模式打开文件。如果打开失败,直接返回false
。 -
安全检查: 对文件名进行安全检查,防止恶意代码伪装成 WordPress 核心文件上传。如果文件名和 WordPress 核心文件相同,则返回
false
。 -
读取文件内容: 使用
fread()
函数读取文件的前 4KB 内容。读取这么多的目的是为了覆盖大部分文件类型的头部信息。 -
处理 Unicode BOM: 检查文件是否包含 Unicode 字节顺序标记(BOM)。如果包含,则移除 BOM。
-
匹配文件头: 遍历允许的 MIME 类型列表,检查文件头是否匹配已知的 MIME 类型签名。这里使用了
strpos()
函数来判断文件头是否以特定的字节序列开头。-
常见文件类型的签名:
MIME 类型 签名 image/jpeg
xFFxD8xFF
image/gif
x47x49x46x38
(GIF8)image/png
x89PNGx0dx0ax1ax0a
application/pdf
%PDF-
-
-
找到匹配项: 如果找到了匹配的文件头,就认为找到了文件类型,提取扩展名,关闭文件,然后返回包含扩展名、MIME 类型和修正后的文件名的数组。
-
未找到匹配项: 如果遍历完所有的 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 类型签名进行匹配。
思考题:
- MIME 嗅探技术有哪些局限性?例如,对于某些文件类型,文件头可能不够明确,或者容易被伪造。
- 除了文件头,还有哪些方法可以用来判断文件类型?例如,可以使用第三方库来分析文件内容,或者使用机器学习模型来进行分类。
- 如何在 WordPress 中自定义允许上传的文件类型?你可以使用
upload_mimes
过滤器来实现。
好了,今天的讲座就到这里。希望大家通过这次学习,对 WordPress 的文件类型判断机制有了更深入的了解。记住,编程的世界充满了乐趣,只要你肯探索,就能发现更多的秘密!下次再见!