月度归档:2012年03月

PHP内核中文件上传类型的获取过程

我们在做WEB应用开发时,经常会遇到文件上传的需求,文件作为一种中间介质将一些信息传递给我们。以PHP为例,如果我们需要实现一个简单的文件上传(假设我们的测试服务器为Apache),首先我们需要有一个前台页面让用户选择文件,这里以一个文件的上传为例:

<form name="upload" action="upload_test.php" method="POST" enctype="multipart/form-data">
<input type="hidden" value="1024" name="MAX_FILE_SIZE" />
请选择文件:<input name="ufile" type="file" />
<input type="submit" value="Just Upload it" />
</form>

当我们选择点击提交按钮时,浏览器会将数据提交给服务器。通过Filddle我们可以看到其提交的请求头如下:

POST http://localhost/test/upload_test.php HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 1347
Cache-Control: max-age=0
Origin: http://localhost
User-Agent: //省略若干
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBq7AMhcljN14rJrU 
 
// 上面的是关键
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost/test/test.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
 
// 以下为POST提交的内容
 
------WebKitFormBoundaryBq7AMhcljN14rJrU
Content-Disposition: form-data; name="MAX_FILE_SIZE"
 
10240
------WebKitFormBoundaryBq7AMhcljN14rJrU
Content-Disposition: form-data; name="ufile"; filename="logo.png"
Content-Type: image/png //这里就是我们想要的文件类型
 
//以下为文件内容

如果我们在upload_test.php文件中打印$_FILES,可以看到上传文件类型为image/png。

对应上面的请求头,image/png在文件内容输出的前面的Content-Type字段中。

基本上我们知道了上传的文件类型是浏览器自己识别,直接以文件的Content-Type字段传递给服务器。那么这些内容在PHP中是如何解析的呢?

文件类型获取过程

当客户端发起文件提交请求时,Apache会将所接收到的内容转交给mod_php5模块。
当PHP接收到请求后,首先会调用sapi_activate,在此函数中程序会根据请求的方法处理数据,如我们示例的POST的方法:

if(!strcmp(SG(request_info).request_method, "POST")
&& (SG(request_info).content_type)) {
/* HTTP POST -> may contain form data to be read into variables
depending on content type given
*/
sapi_read_post_data(TSRMLS_C);
}

sapi_read_post_data在main/SAPI.c中实现,它会根据POST内容的Content-Type类型来选择处理POST内容的方法。

if (zend_hash_find(&SG(known_post_content_types), content_type,
content_type_length+1, (void **) &post_entry) == SUCCESS) {
/* found one, register it for use */
SG(request_info).post_entry = post_entry;
post_reader_func = post_entry->post_reader;
}

以上代码的关键在于SG(known_post_content_types)变量在哪里初始化,其基本过程如下:

sapi_startup
sapi_globals_ctor(&sapi_globals);
php_setup_sapi_content_types(TSRMLS_C);
sapi_register_post_entries(php_post_entries TSRMLS_CC);

这里的的php_post_entries定义在main/php_content_types.c文件。如下:

/* {{{ php_post_entries[]
*/
static sapi_post_entry php_post_entries[] = {
{ DEFAULT_POST_CONTENT_TYPE, sizeof(DEFAULT_POST_CONTENT_TYPE)-1, sapi_read_standard_form_data, php_std_post_handler },
{ MULTIPART_CONTENT_TYPE, sizeof(MULTIPART_CONTENT_TYPE)-1, NULL, rfc1867_post_handler },
{ NULL, 0, NULL, NULL }
};
/* }}} */
 
#define MULTIPART_CONTENT_TYPE "multipart/form-data"
 
#define DEFAULT_POST_CONTENT_TYPE "application/x-www-form-urlencoded"

嗯,这里的MULTIPART_CONTENT_TYPE(multipart/form-data)所对应的rfc1867_post_handler方法就是我们今天要找的核心函数,其定义在main/rfc1867.c文件:SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)

后面获取Content-Type的过程就比较简单了:

  • 通过multipart_buffer_eof控制循环,遍历所有的multipart部分
  • 通过multipart_buffer_headers获取multipart部分的头部信息
  • 通过php_mime_get_hdr_value(header, “Content-Type”)获取类型
  • 通过register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC);将数据写到$_FILES变量。
SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
{
 
//若干省略
    while (!multipart_buffer_eof(mbuff TSRMLS_CC)){
        if (!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) {
		goto fileupload_done;
	}
//若干省略
	/* Possible Content-Type: */
	if (cancel_upload || !(cd = php_mime_get_hdr_value(header, "Content-Type"))) {
		cd = "";
	} else { 
	/* fix for Opera 6.01 */
		s = strchr(cd, ';');
		if (s != NULL) {
			*s = '\0';
		}
	}
//若干省略
	/* Add $foo[type] */
	if (is_arr_upload) {
        	snprintf(lbuf, llen, "%s[type][%s]", abuf, array_index);
	} else {
		snprintf(lbuf, llen, "%s[type]", param);
	}
	register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC);
    //若干省略
    }
}

其它的$_FILES中的size、name等字段,其实现过程与type类似