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

各位观众老爷们,晚上好!今天咱们来聊聊WordPress里一个挺有意思的函数 wp_check_filetype(),重点说说它怎么通过文件头(MIME类型签名)来判断文件类型,而不是简单地看扩展名。这就像咱们识别人一样,不能光看发型和衣服,还得看看脸,看看DNA!

开场白:扩展名靠不住,信头才是王道

在Web开发这片江湖里,判断文件类型是个基本需求。最常见的套路就是看文件名后缀,比如 .jpg 就是图片,.mp3 就是音频。但这种方法太容易被忽悠了。随便把一个 .exe 文件改成 .jpg,岂不是就蒙混过关了?太天真了!

为了更靠谱地识别文件类型,就得靠文件头(也叫魔数、MIME类型签名)。文件头是文件开头的一段字节,它就像文件的指纹,能唯一标识文件类型。即使你把文件扩展名改得面目全非,文件头还是在那里,默默地诉说着文件的真实身份。

wp_check_filetype():WordPress里的文件类型侦探

wp_check_filetype() 函数是WordPress里专门负责文件类型判断的。它会综合考虑文件名和文件头,最终确定文件的MIME类型和扩展名。

咱们先来看看这个函数的原型:

/**
 * Retrieve file type based on extension name and mime type.
 *
 * @since 2.0.0
 *
 * @param string      $filename Filename of the file.
 * @param string|null $mimes    Optional. Array of mime types keyed by their file extension regexps.
 * @return array {
 *     @type string|false $ext         The file extension. False, if the file extension cannot be determined.
 *     @type string|false $type        The mime type. False, if the mime type cannot be determined.
 *     @type string|false $proper_filename The proper filename without any weird characters. False, if the proper filename cannot be determined.
 * }
 */
function wp_check_filetype( $filename, $mimes = null ) {
    // ... 函数体 ...
}
  • $filename: 待检测的文件名(包含路径)。
  • $mimes: 可选参数,自定义的MIME类型数组。如果没传,就用WordPress默认的。

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

  • ext: 文件扩展名。
  • type: 文件的MIME类型。
  • proper_filename: 修正后的文件名(如果文件名包含特殊字符)。

源码剖析:一步一步揭开真相

接下来,咱们深入 wp_check_filetype() 函数的源码,看看它是怎么工作的。为了方便讲解,我把源码简化一下,只保留核心逻辑。

function wp_check_filetype( $filename, $mimes = null ) {

    // 1. 获取文件名和扩展名
    $wp_filetype = wp_check_filetype_and_ext( $filename, $mimes );
    $ext = $wp_filetype['ext'];
    $type = $wp_filetype['type'];
    $proper_filename = $wp_filetype['proper_filename'];

    if ( ! empty( $type ) ) {
        return array(
            'ext' => $ext,
            'type' => $type,
            'proper_filename' => $proper_filename,
        );
    }

    // 2. 如果通过扩展名无法判断,尝试通过文件头判断
    if ( empty( $type ) && function_exists( 'wp_get_mime_types' ) && function_exists( 'wp_read_image_metadata' )) {
        $mime_types = wp_get_mime_types();

        if ( function_exists( 'wp_getimagesize' ) ) {
            $imagesize = @wp_getimagesize( $filename );
            if ( !empty( $imagesize ) ) {
                $type = $imagesize['mime'];
            }
        }

        // 3. 最后实在不行,就返回空
        if ( empty( $type ) ) {
            $file = @fopen( $filename, 'rb' );
            if ( $file ) {
                $byte = fread( $file, 256 );
                fclose( $file );
                $type = wp_mime_type_by_contents( $filename, $byte, $mime_types );
            }
        }
    }

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

第一步:wp_check_filetype_and_ext() 获取扩展名和初始MIME类型

wp_check_filetype() 函数首先调用 wp_check_filetype_and_ext() 函数,这个函数主要根据文件名后缀来判断文件类型。

function wp_check_filetype_and_ext( $filename, $mimes = null ) {
  $type = false;
  $ext = false;
  $proper_filename = false;

  // 1. 获取文件名和扩展名
  $file = wp_basename( $filename );
  $parts = explode( '.', $file );
  if ( count( $parts ) < 2 ) {
    return array( 'ext' => $ext, 'type' => $type, 'proper_filename' => $proper_filename );
  }

  $ext = strtolower( array_pop( $parts ) );

  // 2. 获取MIME类型列表
  if ( empty( $mimes ) ) {
    $mimes = get_allowed_mime_types();
  }

  // 3. 根据扩展名查找MIME类型
  if ( isset( $mimes[ $ext ] ) ) {
    $type = $mimes[ $ext ];
  }

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

这个函数做了以下几件事:

  1. 提取扩展名: 它会从文件名中提取出扩展名,比如 example.jpg 提取出 jpg
  2. 获取MIME类型列表: 它会获取WordPress允许上传的MIME类型列表。这个列表是一个数组,key是扩展名,value是MIME类型。
  3. 根据扩展名查找MIME类型: 它会在MIME类型列表中查找与扩展名对应的MIME类型。如果找到了,就认为这个文件是该类型。

举个例子:

假设 $filenameimage.jpg,WordPress的MIME类型列表中 jpg 对应的MIME类型是 image/jpeg。那么,wp_check_filetype_and_ext() 函数就会返回:

array(
    'ext' => 'jpg',
    'type' => 'image/jpeg',
    'proper_filename' => false,
)

第二步:文件头大显身手: wp_mime_type_by_contents()

如果 wp_check_filetype_and_ext() 函数无法确定文件类型(比如扩展名不存在,或者MIME类型列表中没有对应的条目),wp_check_filetype() 函数就会尝试读取文件头,通过 wp_mime_type_by_contents() 函数来判断文件类型。

function wp_mime_type_by_contents( $filename, $contents, $mime_types = array() ) {

    // 1. 默认的MIME类型规则
    $mime = false;

    // 2. 遍历MIME类型规则,查找匹配的文件头
    foreach ( (array) wp_get_mime_types() as $extensions => $mime_regex ) {

        $matches = null;
        if ( preg_match( $mime_regex, $contents, $matches ) ) {
            $mime = trim( current( explode( ' ', $mime_regex ) ) );
            break;
        }
    }

    // 3. 如果找到匹配的规则,返回MIME类型
    return $mime;
}

这个函数的核心思想是:

  1. MIME类型规则: 定义了一系列的MIME类型规则,每个规则都包含一个正则表达式,用于匹配文件头。
  2. 匹配文件头: 函数会遍历这些规则,用正则表达式去匹配文件头。如果匹配成功,就认为这个文件是该类型。

关键在于 wp_get_mime_types() 函数返回的MIME类型规则数组。这个数组的结构是这样的:

array(
    'jpg|jpeg|jpe' => 'image/jpeg',
    'gif' => 'image/gif',
    'png' => 'image/png',
    'bmp' => 'image/bmp',
    'tiff|tif' => 'image/tiff',
    'ico' => 'image/x-icon',
    'asf|asx|wax|wmv|wmx' => 'video/asf',
    'avi' => 'video/avi',
    'divx' => 'video/divx',
    'mov|qt' => 'video/quicktime',
    'mpeg|mpg|mpe' => 'video/mpeg',
    'mp4|m4v' => 'video/mp4',
    'ogv' => 'video/ogg',
    'webm' => 'video/webm',
    'mkv' => 'video/x-matroska',
    '3gp|3gpp' => 'video/3gpp',
    '3g2|3gp2' => 'video/3gpp2',
    'txt|asc|c|cc|h' => 'text/plain',
    'csv' => 'text/csv',
    'rtx' => 'text/richtext',
    'css' => 'text/css',
    'htm|html' => 'text/html',
    'mp3|m4a|m4b' => 'audio/mpeg',
    'mpga' => 'audio/mpeg',
    'ogg|oga' => 'audio/ogg',
    'wav' => 'audio/wav',
    'wma' => 'audio/x-ms-wma',
    'wmx' => 'audio/x-ms-wmx',
    'mka' => 'audio/x-matroska',
    'ra|ram' => 'audio/x-realaudio',
    'mid|midi' => 'audio/midi',
    'rtf' => 'application/rtf',
    'js' => 'application/javascript',
    'pdf' => 'application/pdf',
    'swf' => 'application/x-shockwave-flash',
    'class' => 'application/java',
    'tar' => 'application/x-tar',
    'zip' => 'application/zip',
    'gz|gzip' => 'application/x-gzip',
    'rar' => 'application/rar',
    '7z' => 'application/x-7z-compressed',
    'exe' => 'application/x-msdownload',
    'psd' => 'image/vnd.adobe.photoshop',
    'ai' => 'application/postscript',
    'torrent' => 'application/x-bittorrent',
    'woff' => 'application/font-woff',
    'woff2' => 'application/font-woff2',
    'eot' => 'application/vnd.ms-fontobject',
    'ttf' => 'application/x-font-ttf',
    'otf' => 'application/x-font-opentype',
    'svg' => 'image/svg+xml',
    'svgz' => 'image/svg+xml',
    'webp' => 'image/webp'
);

等等,这里好像没有看到通过文件头判断类型的信息啊?别急,玄机在于value值,这些value的值,实际上是可以通过正则匹配文件头的。

深入挖掘:MIME类型规则的奥秘

要理解 wp_mime_type_by_contents() 函数的工作原理,就必须理解MIME类型规则的含义。咱们以几个常见的MIME类型为例:

扩展名 MIME类型 文件头特征
jpg|jpeg|jpe image/jpeg ^xFFxD8xFF (JPEG文件的开头通常是 FF D8 FF)
gif image/gif ^GIF8[79]a (GIF文件的开头是 GIF87aGIF89a)
png image/png ^x89PNGx0dx0ax1ax0a (PNG文件的开头是 89 50 4E 47 0D 0A 1A 0A)
pdf application/pdf ^%PDF (PDF文件的开头是 %PDF)

这些文件头特征都是用十六进制表示的,它们是文件格式规范的一部分。wp_mime_type_by_contents() 函数会读取文件的前几个字节,然后用正则表达式去匹配这些特征。如果匹配成功,就说明这个文件是对应的类型。

举个栗子:识别JPEG文件

假设咱们有一个文件 test.jpg,它的文件头是 FF D8 FF E0 00 10 4A 46 49 46 00 01...wp_mime_type_by_contents() 函数会读取文件的前256个字节,然后用 ^xFFxD8xFF 这个正则表达式去匹配。由于文件头的前三个字节是 FF D8 FF,所以匹配成功,函数就会返回 image/jpeg

第三步:其他判断方法

如果通过 wp_mime_type_by_contents() 函数仍然无法判断文件类型,wp_check_filetype() 函数还会尝试使用 wp_getimagesize() 函数来判断图片类型。这个函数可以读取图片文件的头部信息,获取图片的尺寸和MIME类型。

总结:多管齐下,确保万无一失

wp_check_filetype() 函数通过多种手段来判断文件类型,确保准确性和安全性:

  1. 优先使用扩展名: 如果扩展名存在且在MIME类型列表中,就直接使用扩展名对应的MIME类型。
  2. 文件头验证: 如果扩展名不可靠,就读取文件头,用正则表达式匹配MIME类型规则。
  3. wp_getimagesize() 函数: 对于图片文件,还可以使用 wp_getimagesize() 函数来获取MIME类型。

通过这些方法,wp_check_filetype() 函数可以有效地防止恶意文件上传,保障网站的安全。

代码示例:模拟 wp_check_filetype() 的行为

为了更好地理解 wp_check_filetype() 函数的工作原理,咱们来写一个简单的PHP脚本,模拟它的行为。

<?php

function my_check_filetype( $filename ) {
    // 1. 获取扩展名
    $file = basename( $filename );
    $parts = explode( '.', $file );
    if ( count( $parts ) < 2 ) {
        return false;
    }
    $ext = strtolower( array_pop( $parts ) );

    // 2. 获取MIME类型列表
    $mime_types = array(
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'pdf' => 'application/pdf',
    );

    // 3. 根据扩展名查找MIME类型
    if ( isset( $mime_types[ $ext ] ) ) {
        return $mime_types[ $ext ];
    }

    // 4. 读取文件头
    $file = fopen( $filename, 'rb' );
    if ( ! $file ) {
        return false;
    }
    $contents = fread( $file, 256 );
    fclose( $file );

    // 5. 根据文件头判断MIME类型
    if ( preg_match( '/^xFFxD8xFF/', $contents ) ) {
        return 'image/jpeg';
    } elseif ( preg_match( '/^GIF8[79]a/', $contents ) ) {
        return 'image/gif';
    } elseif ( preg_match( '/^x89PNGx0dx0ax1ax0a/', $contents ) ) {
        return 'image/png';
    } elseif ( preg_match( '/^%PDF/', $contents ) ) {
        return 'application/pdf';
    }

    return false;
}

// 测试
$filename = 'test.jpg'; // 请替换成你的测试文件
$mime_type = my_check_filetype( $filename );

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

?>

这个脚本会先根据扩展名判断文件类型,如果无法判断,就读取文件头,用正则表达式匹配常见的MIME类型。

总结

wp_check_filetype() 函数是WordPress里一个非常重要的函数,它负责判断文件类型,防止恶意文件上传。通过深入剖析它的源码,我们可以更好地理解文件类型判断的原理,以及如何通过文件头来识别文件类型。

希望今天的讲座对大家有所帮助!记住,在Web开发的世界里,安全永远是第一位的。下次再见!

发表回复

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