haldayne/customs

$_FILES 超全局变量的迭代器,以及一个用于验证文件上传的强大 API。

1.0.8 2016-07-20 13:29 UTC

This package is not auto-updated.

Last update: 2024-09-10 19:59:22 UTC


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.";
}

等等...

现实世界并不那么简单。上传文件充满了失败模式

  1. $_FILES 中的数据是否有效?PHP 本身或您的应用程序中的错误可能会使您容易受到攻击。
  2. 服务器是否能够存储文件?您可能没有启用上传,或者服务器磁盘空间不足。
  3. 文件是否完全接收?网络连接可能已中断,或指定的文件大小超过了允许的范围。
  4. 接收到的文件是否符合业务规则?文件可能太小或没有支持的 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。您想做什么?

  1. 在根本级别检查文件。调用 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