haldayne / customs
$_FILES 超全局变量的迭代器,以及一个用于验证文件上传的强大 API。
Requires
- php: ^5.5.0 || ^7.0
- haldayne/boost: ^1.0
Requires (Dev)
- fzaninotto/faker: ^1.5
- mikey179/vfsstream: ^1.5
- phpunit/phpunit: ~4.0
README
接收用户文件是一个常见需求,但遗憾的是 PHP 并没有提供统一的接口来访问文件。根据文件的上传方式(单个文件、多个不同命名的文件,或多个同名的文件),$_FILES
超全局变量将采用不同的结构。更糟糕的是,根据您的服务器配置,您可能无法可靠地确定上传文件的类型。
处理这些情况,同时在防御基于上传的攻击,需要大量的代码。这个库提供了一个简单的迭代器来访问上传的文件,并提供了一个强大的 API 来检查上传。
开始吧!
您需要至少 PHP 5.5.0。不需要其他扩展。
使用 composer 安装:php composer.phar require haldayne/customs ^1.0
迭代上传文件
Haldayne\Customs\UploadIterator
提供了一种非常简单的方式来处理上传文件
use Haldayne\Customs; $uploads = new UploadIterator(); foreach ($uploads as $file) { $stored_path = $file->moveTo('/path/to/folder/'); echo "The file was permanently stored at $stored_path."; }
等等...
现实世界并不那么简单。上传文件充满了失败模式
- 在
$_FILES
中的数据是否有效?PHP 本身或您的应用程序中的错误可能会使您容易受到攻击。 - 服务器是否能够存储文件?您可能没有启用上传,或者服务器磁盘空间不足。
- 文件是否完全接收?网络连接可能已中断,或指定的文件大小超过了允许的范围。
- 接收到的文件是否符合业务规则?文件可能太小或没有支持的 MIME 类型。
如果 UploadIterator
检测到安全违规或服务器问题,它将抛出 UploadException
。Customs 认为,这些是异常情况,您需要处理。另一方面,如果上传的文件不完整,因为用户的浏览器未能提供整个文件,您将在迭代器中找到一个 UploadError
对象。否则,迭代器将使用 UploadFile
对象包装上传。以下是一个更健壮的示例
use Haldayne\Customs; try { $uploads = new UploadIterator(); } catch (ServerProblemException $ex) { // uh oh, your server has a problem: no temp dir for storing files, // couldn't write file, extension prevents upload, etc. You need to // handle this, because the user didn't do anything wrong. throw $ex; } catch (SecurityConcernException $ex) { // uh oh, the user provided something that looks like an attempt to // break in. You might want to just rethrow this exception, or maybe // you want to honeypot the user. error_log("Break in attempt"); echo "Your file received"; return; } foreach ($uploads as $file) { if ($file instanceof UploadError) { echo 'Sorry, your upload was not stored.'; // you can discover the original HTML name for the file input echo $file->getHtmlName(); // you can emit a generic message... echo $file->getErrorMessage(); // ... or you can get specific. if ($file->isTooBig($maximum)) { echo "The file was too big. Maximum is $maximum bytes."; } else if ($file->isPartial($received)) { echo "The file upload wasn't complete. Only $received bytes received."; } else if ($file->notUploaded()) { echo "No file uploaded."; } } else { // $file is an instance of UploadFile: now you can check for domain- // specific errors if ($file->getServerFile()->getFileSize() < 100) { echo "Minimum file size is 100 bytes."; } else if (! $file->getMimeAnalyzer()->isAnImage()) { echo "You must upload an image file."; } else { $file->moveTo('/path/to/images'); } } }
上传处理很复杂,但 Customs 将处理 $_FILES
超全局变量的复杂性、条件性缺失 MIME 扩展等语言复杂性从开发人员那里推出去。
与上传文件一起工作
所以 UploadIterator
给您提供了一个 UploadFile
。您想做什么?
- 在根本级别检查文件。调用
UploadFile::getServerFile()
,它是一个 [SplFileInfo
][4]
接下来呢?代码通常做的第一件事是检查文件是否与预期的类型匹配。MIME 通常可以完成这项工作,但并不总是如此。我曾经编写了很多使用外部空间分析工具的 GIS 代码。然而,有几个问题。首先
通常您会做两件事:使用系统工具检查文件,然后将文件移动到某个目录以供以后使用。Customs
对于许多情况,检查文件大小和 MIME 类型就足够了。
所以用户上传了一个文件:没有抛出异常,并且您有一个 UploadFile
。接下来做什么?通常,您会对文件做两件事
特性
处理上传文件是一个常见任务,但处理 $_FILES
超全局变量 并不容易。常见的任务应该容易。如果它们不容易,就使它们变得容易!
处理 $_FILES
分裂形态
根据您如何编写HTML表单,$_FILES
超全局变量有不同的格式。如果表单有两个具有不同名称的文件输入,则$_FILES
有两个元素
<input type='file' name='fileA' />
<input type='file' name='fileB' />
/* // $_FILES = array ( // notice two outer elements "fileA" and "fileB"
"fileA" => array (
"name" => "cat.png", // notice all these are string keys
"type" => "image/png",
"tmp_name" => "/tmp/phpZuLGPe",
"error" => 0,
"size" => 35669
),
"fileB" => array (
"name" => "dog.png",
"type" => "image/png",
"tmp_name" => "/tmp/phpUee89j",
"error" => 0,
"size" => 43225
)
)
*/
这很简单。但是具有数组名称的文件输入则不同
<input type='file' name='file[A]' />
<input type='file' name='file[B]' />
/* // $_FILES = array (
"file" => array ( // notice only one outer element
"name" => array ( // with array keys!
"A" => "cat.png",
"B" => "dog.png"
),
"type" => array (
"A" => "image/png",
"B" => "image/png"
),
"tmp_name" => array (
"A" => "/tmp/phpZuLGPe",
"B" => "/tmp/phpUee89j"
),
"error" => array (
"A" => 0,
"B" => 0
),
"size" => array (
"A" => 35669,
"B" => 43225
)
)
)
*/
如果你这么做,上帝保佑你
<input type='file' name='file[a][b][c][d]' /> // six levels deep
<input type='file' name='file 1' /> // one level, key named "file_1"
<input type='file' name='file 1[.1]' /> // two levels, keys "file_1" => ".1"
// these next three all resolve to the same key "file_X"
<input type='file' name='file X' />
<input type='file' name='file.X' />
<input type='file' name='file_X' />
这是三种不同的场景,所有这些场景都取决于您的表单。作为一个用户端开发者,我更喜欢有一种单一的迭代路径来处理这种结构,无论表单如何请求文件。这是基本的表现与逻辑分离。这也是库的第一个特性:一个迭代器统治一切!
处理错误和其他常见上传问题
现在下一个问题:上传文件时可能会出很多问题。客户端可能给你文件太少或太多。文件可能太大或太小。或者MIME类型错误。您的服务器可能配置错误,或者磁盘空间不足。这些都是if
条件。您,作为开发者,需要处理每一个。
由于$_FILES
仅仅是一个数据数组,您没有整洁的对象方法可以调用以检测这些条件。如果您没有安装finfo
扩展,您必须编写一个单独的分支,回退到file
二进制或其他类型的MIME类型逻辑。这只是一大堆工作。
这个库将这些相关功能捆绑在一起,以易于使用的对象方法提供,这样您就可以提出问题并继续您的应用程序代码。您可以
- 统计总共上传了多少个文件,或按HTML名称统计
- 获取文件的最终MIME类型,或请求MIME类型的猜测
- 决定文件是否太大以至于系统无法接受,或是否部分上传
- 区分客户端错误和服务器错误
减少开发者错误的机会
$_FILES
中的信息不可信。开发者不应存储使用客户端提供的名称命名的文件,因为这些名称可能包含不安全的字符。为了鼓励这种理念,这个库采取了一种默认的防御姿态
use Haldayne\UploadIterator;
foreach (new UploadIterator as $file) {
if ($file instanceof UploadFile) {
$stored_path = $file->move('/path/to/folder/');
// you choose "where" the file goes, not what it will be named
// $stored_path === /path/to/folder/69c779f0746503ba7e42f87ce1e91152.png
}
}
存储的文件被赋予一个随机、唯一的文件名,保留了原始文件的扩展名。但如果你想知道原始文件名怎么办?你有两个选择:(a)自己做些事情来存储这些元数据,或(b)使用moveTo
创建的元数据文件。
因此move
创建了一个小元数据文件,位于上传文件旁边。元数据文件只是一个包含所有原始文件信息的PHP数组
$ ls /path/to/folder
69c779f0746503ba7e42f87ce1e91152.png
69c779f0746503ba7e42f87ce1e91152.png.meta
$ cat /path/to/folder/69c779f0746503ba7e42f87ce1e91152.png.meta
<?php return array (
'name' => 'Picture of my Cat.png',
'type' => 'image/png',
'size' => 35889,
'date' => 10987685941
);
TODO:这是一个安全风险,有人可能会偷取元数据。但他们也可以偷取所有其他内容。这很糟糕吗?我们如何帮助防止偷窃?
相关项目
👽 ➖ 一个简单的Symfony2扩展包,用于简化使用ORM实体和ODM文档的文件上传。
VichUploaderBundle是一个Symfony2扩展包,旨在简化ORM实体、MongoDB ODM文档、PHPCR ODM文档或Propel模型附加的文件上传。
- 自动命名并保存文件到配置的目录
- 在从数据存储加载为
Symfony\Component\HttpFoundation\File\File
实例时,将文件注入到实体或文档中- 在从数据存储中删除实体或文档时,从文件系统中删除文件
- 模板辅助器,用于生成指向文件的公共URL