PHP 文件上传源码分析(RFC1867)


Posted in PHP onOctober 30, 2009

你总不至于在用户要上传头像的时候告诉用户”请打开FTP客户端,上传文件到https://3water.com/uploads/中, 并以2dk433423l.jpg命名”吧?

而基于HTTP的上传,相对来说易用性和安全性上就比FTP要增强了很多. 可以应用的上传方式有PUT, WEBDAV, 和RFC1867三种, 本文将分析在PHP中,是如何基于RFC1867实现文件上传的.

RFC1867

RCF1867是Form-based File Upload in HTML标准协议, RFC1867标准对HTML做出了两处修改:
 

1 为input元素的type属性增加了一个file选项。
2 input标记可以具有accept属性,该属性能够指定可被上传的文件类型或文件格式列表。

另外,本标准还定义了一种新的mime类型:multipart/form-data,以及当处理一个带有enctype=”multipart/form-data” 并且/或含有<input type=”file”>的标记的表单时所应该采取的行为。

举例来说,当HTML想让用户能够上传一个或更多的文件时,他可以这么写:

<form enctype="multipart/form-data" action="upload.php" method=post>
选择文件:
<input name="userfile" type="file">
文件描述:
<input name="description" type="text">
<input type="submit" value="上传">
</form>

这个表单, 大家一定不陌生, 而对于PHP来说, 它自己另外定义了一个默认表单元素MAX_FILE_SIZE, 用户可以通过这个隐藏的表单元素来建议PHP最多只容许上传文件的大小, 比如对于上面的例子, 我们希望用户上传的文件不能大于5000(5k)字节, 那么可以如下写:

<form enctype="multipart/form-data" action="upload.php" method=post>
<input type="hidden" value="5000" name="MAX_FILE_SIZE"> <!--文件大小-->
选择文件:
<input name="userfile" type="file">
文件描述:
<input name="description" type="text">
<input type="submit" value="上传">
</form>

姑且不说, 这个MAX_FILE_SIZE是多么的不可靠(所以基于浏览器的控制,都是不可靠的), 单纯从实现来讲, 我会慢慢介绍这个MAX_FILE_SIZE是如何起作用的.

当用户选择了一个文件(laruence.txt), 并填写好文件描述(”laruence的个人介绍”), 点击上传后, 发生了什么呢?

表单提交

在用户确定提交以后, 浏览器会发送如下类似格式的数据包到form中action属性指定的页面(在本例中是upload.php):

//请求头
POST /upload.php HTTP/1.0\r\n
...
Host: www.laruence.com\r\n
...
Content-length: xxxxx\r\n
...
Content-type: multipart/form-data, boundary=--------------7d51863950254\r\n
...\r\n\r\n
//开始POST数据内容
---------------7d51863950254
content-disposition: form-data; name="description"
laruence的个人介绍
---------------7d51863950254
content-disposition: form-data; name="userfile"; filename="laruence.txt"
Content-Type: text/plain
... laruence.txt 的内容...
---------------7d51863950254

接下来, 就是服务器, 是如何处理这些数据了.

接受上传

当Web服务器, 此处假设为Apache(另外假设PHP是以module方式安装在Apache上的), 接受到用户的数据时, 首先它根据HTTP请求头, 通过确定MIME TYPE为PHP类型, 然后经过一些过程以后(这部分,可以参看我之前的PHP Life Cycle ppt), 最终会把控制权交给PHP模块.

这个时候, PHP会调用sapi_activate来初始化一个请求, 在这个过程中, 首先判断请求类型, 此时是POST, 从而去调用sapi_read_post_data, 通过Content-type, 找到rfc1867的处理函数rfc1867_post_handler, 从而调用这个handler, 来分析POST来的数据.

关于rfc1867_post_handler这部分的源代码, 可以在mian/rfc1867.c找到, 另外也可以参看我之前的深入理解PHP之文件上传, 其中也列出的源代码.

然后, PHP通过boundary, 对于每一个分段, 都通过检查, 是否同时定义了:

name和filename属性(有名文件上传)
 没有定义name定义了filename(无名上传)
 定义了name没有定义filename(普通数据),

从而进行不同的处理.

if ((cd = php_mime_get_hdr_value(header, "Content-Disposition"))) {
 char *pair=NULL;
 int end=0; while (isspace(*cd)) {
 ++cd;
 }
 while (*cd && (pair = php_ap_getword(&cd, ';')))
 {
 char *key=NULL, *word = pair;
 while (isspace(*cd)) {
 ++cd;
 }
 if (strchr(pair, '=')) {
 key = php_ap_getword(&pair, '=');
 if (!strcasecmp(key, "name")) {
 //获取name字段
 if (param) {
 efree(param);
 }
 param = php_ap_getword_conf(&pair TSRMLS_CC);
 } else if (!strcasecmp(key, "filename")) {
 //获取filename字段
 if (filename) {
 efree(filename);
 }
 filename = php_ap_getword_conf(&pair TSRMLS_CC);
 }
 }
 if (key) {
 efree(key);
 }
 efree(word);
 }

在这个过程中, PHP会去检查普通数据中,是否有MAX_FILE_SIZE.

/* Normal form variable, safe to read all data into memory */
if (!filename && param) {
 unsigned int value_len;
 char *value = multipart_buffer_read_body(mbuff, &value_len TSRMLS_CC);
 unsigned int new_val_len; /* Dummy variable */
 ...... if (!strcasecmp(param, "MAX_FILE_SIZE")) {
 max_file_size = atol(value);
 }
 efree(param);
 efree(value);
 continue;
}

有的话, 就会按照它的值来检查文件大小是否超出.

if (PG(upload_max_filesize) > 0 && total_bytes > PG(upload_max_filesize)) {
 cancel_upload = UPLOAD_ERROR_A;
} else if (max_file_size && (total_bytes > max_file_size)) {
#if DEBUG_FILE_UPLOAD
 sapi_module.sapi_error(E_NOTICE,
 "MAX_FILE_SIZE of %ld bytes exceeded - file [%s=%s] not saved",
 max_file_size, param, filename);
#endif
 cancel_upload = UPLOAD_ERROR_B;
}

通过上面的代码,我们也可以看到, 判断分为俩部, 第一部分是检查PHP默认的上传上限. 第二部分才是检查用户自定义的MAX_FILE_SIZE, 所以表单中定义的MAX_FILE_SIZE并不能超过PHP中设置的最大上传文件大小.

通过对name和filename的判断, 如果是文件上传, 会根据php的设置, 在文件上传目录中创建一个随机名字的临时文件:

if (!skip_upload) {
 /* Handle file */
 fd = php_open_temporary_fd_ex(PG(upload_tmp_dir),
 "php", &temp_filename, 1 TSRMLS_CC);
 if (fd==-1) {
 sapi_module.sapi_error(E_WARNING,
 "File upload error - unable to create a temporary file");
 cancel_upload = UPLOAD_ERROR_E;
 }
}

返回文件句柄, 和临时随机文件名.

之后, 还会有一些验证,比如文件名合法, name合法等.

如果这些验证都通过, 那么就把内容读入, 写入到这个临时文件中.

.....
else if (blen > 0) {
 wlen = write(fd, buff, blen); //写入临时文件.
 if (wlen == -1) {
 /* write failed */
#if DEBUG_FILE_UPLOAD
 sapi_module.sapi_error(E_NOTICE, "write() failed - %s", strerror(errno));
#endif
 cancel_upload = UPLOAD_ERROR_F;
 }
}
....

当循环读入完成后, 关闭临时文件句柄. 记录临时变量名:

zend_hash_add(SG(rfc1867_uploaded_files), temp_filename,
 strlen(temp_filename) + 1, &temp_filename, sizeof(char *), NULL);

并且生成FILE变量, 这个时候, 如果是有名上传, 那么就会设置:

$_FILES['userfile'] //name="userfile"

如果是无名上传, 则会使用tmp_name来设置:

$_FILES['tmp_name'] //无名上传

最终交给用户编写的upload.php处理.

这时在upload.php中, 用户就可以通过move_uploaded_file来操作刚才生成的文件了~

PHP 相关文章推荐
关于页面优化和伪静态
Oct 11 PHP
PHP之生成GIF动画的实现方法
Jun 07 PHP
PHP stripos()函数及注意事项的分析
Jun 08 PHP
php+js实现异步图片上传实例分享
Jun 02 PHP
CodeIgniter输出中文乱码的两种解决办法
Jun 12 PHP
PHP实现无限级分类(不使用递归)
Oct 22 PHP
php微信公众平台配置接口开发程序
Sep 22 PHP
php实现遍历文件夹的方法汇总
Mar 02 PHP
PHP大文件分割上传 PHP分片上传
Aug 28 PHP
yii2.0整合阿里云oss的示例代码
Sep 19 PHP
php 读取文件夹下所有图片、文件的实例
Oct 17 PHP
PHP中将一个字符串部分字符用星号*替代隐藏的实现代码
Sep 08 PHP
浅谈PHP 闭包特性在实际应用中的问题
Oct 30 #PHP
php实现jQuery扩展函数
Oct 30 #PHP
PHP 读取和修改大文件的某行内容的代码
Oct 30 #PHP
PHP 批量删除数据的方法分析
Oct 30 #PHP
ThinkPHP php 框架学习笔记
Oct 30 #PHP
php pack与unpack 摸板字符字符含义
Oct 29 #PHP
php 显示指定路径下的图片
Oct 29 #PHP
You might like
Optimizer与Debugger兼容性问题的解决方法
2008/12/01 PHP
php获取目录中所有文件名及判断文件与目录的简单方法
2017/03/04 PHP
Laravel框架实现的使用smtp发送邮件功能示例
2019/03/12 PHP
js调用后台、后台调用前台等方法总结
2014/04/17 Javascript
JavaScript对象反射用法实例
2015/04/17 Javascript
jquery 遍历数组 each 方法详解
2016/05/25 Javascript
JavaScript中两个字符串的匹配
2016/06/08 Javascript
JavaScript实现向select下拉框中添加和删除元素的方法
2017/03/07 Javascript
AngularJS实现图片上传和预览功能的方法分析
2017/11/08 Javascript
jQuery+ajax读取json数据并按照价格排序示例
2018/03/28 jQuery
vue js秒转天数小时分钟秒的实例代码
2018/08/08 Javascript
vue 自定义指令自动获取文本框焦点的方法
2018/08/25 Javascript
解决vue中修改了数据但视图无法更新的情况
2018/08/27 Javascript
微信小程序自定义select下拉选项框组件的实现代码
2018/08/28 Javascript
[02:40]2014DOTA2 国际邀请赛中国区预选赛 四大豪门抵达华西村
2014/05/23 DOTA
爬山算法简介和Python实现实例
2014/04/26 Python
详解python使用递归、尾递归、循环三种方式实现斐波那契数列
2018/01/16 Python
python flask实现分页的示例代码
2018/08/02 Python
详解Python 装饰器执行顺序迷思
2018/08/08 Python
Python中垃圾回收和del语句详解
2018/11/15 Python
python实现的爬取电影下载链接功能示例
2019/08/26 Python
Python 之 Json序列化嵌套类方式
2020/02/27 Python
详解python3类型注释annotations实用案例
2021/01/20 Python
使用CSS3制作一个简单的Chrome模拟器
2015/07/15 HTML / CSS
会计专业个人求职信范文
2014/01/08 职场文书
大学班级干部的自我评价分享
2014/02/10 职场文书
生日宴会主持词
2014/03/20 职场文书
村干部群众路线教育活动对照检查材料
2014/10/01 职场文书
小学母亲节活动总结
2015/02/10 职场文书
学校中秋节活动总结
2015/03/23 职场文书
拾金不昧表扬信怎么写
2015/05/04 职场文书
毕业赠语大全
2015/06/23 职场文书
2016年5月份红领巾广播稿
2015/12/21 职场文书
使用 Docker Compose 构建复杂的多容器App
2022/04/30 Servers
python神经网络ResNet50模型
2022/05/06 Python
MySQL的prepare使用以及遇到的bug
2022/05/11 MySQL