PHP Conference Japan 2024

POST 方法上传

此功能允许用户上传文本和二进制文件。结合 PHP 的身份验证和文件操作函数,您可以完全控制谁被允许上传以及上传文件后要对其执行的操作。

PHP 能够接收来自任何符合 RFC-1867 的浏览器的文件上传。

注意相关配置说明

另请参阅 file_uploadsupload_max_filesizeupload_tmp_dirpost_max_sizemax_input_time 指令,这些指令位于 php.ini 中。

PHP 还支持 Netscape Composer 和 W3C 的 Amaya 客户端使用的 PUT 方法文件上传。有关更多详细信息,请参阅 PUT 方法支持

示例 #1 文件上传表单

可以通过创建如下所示的特殊表单来构建文件上传屏幕

<!-- The data encoding type, enctype, MUST be specified as below -->
<form enctype="multipart/form-data" action="__URL__" method="POST">
    <!-- MAX_FILE_SIZE must precede the file input field -->
    <input type="hidden" name="MAX_FILE_SIZE" value="30000" />
    <!-- Name of input element determines name in $_FILES array -->
    Send this file: <input name="userfile" type="file" />
    <input type="submit" value="Send File" />
</form>

以上示例中的 __URL__ 应替换为指向 PHP 文件的 URL。

隐藏字段 MAX_FILE_SIZE(以字节为单位)必须位于文件输入字段之前,其值是 PHP 接受的最大文件大小。始终应使用此表单元素,因为它可以避免用户在传输大型文件时浪费时间,最终发现文件过大而传输失败。请记住:在浏览器端欺骗此设置非常容易,因此切勿依赖此功能来阻止较大文件。它仅仅是为客户端应用程序的用户提供的一个便利功能。但是,PHP 设置(服务器端)的最大大小无法被欺骗。

注意:

确保您的文件上传表单具有属性 enctype="multipart/form-data",否则文件上传将无法正常工作。

全局变量 $_FILES 将包含所有上传的文件信息。来自示例表单的内容如下所示。请注意,这假设使用文件上传名称 userfile,如以上示例脚本中所示。这可以是任何名称。

$_FILES['userfile']['name']

客户端计算机上文件的原始名称。

$_FILES['userfile']['type']

文件的 MIME 类型,如果浏览器提供了此信息。例如 "image/gif"。但是,此 MIME 类型在 PHP 端未经检查,因此不要将其值视为理所当然。

$_FILES['userfile']['size']

上传文件的大小(以字节为单位)。

$_FILES['userfile']['tmp_name']

上传文件在服务器上存储的临时文件名。

$_FILES['userfile']['error']

与此文件上传关联的 错误代码

$_FILES['userfile']['full_path']

浏览器提交的完整路径。此值并不总是包含真实的目录结构,并且不可信。从 PHP 8.1.0 开始可用。

默认情况下,文件将存储在服务器的默认临时目录中,除非使用 php.ini 中的 upload_tmp_dir 指令指定了其他位置。可以通过在 PHP 运行的环境中设置环境变量 TMPDIR 来更改服务器的默认目录。在 PHP 脚本中使用 putenv() 设置它将不起作用。此环境变量也可用于确保其他操作也能正常处理上传的文件。

示例 #2 验证文件上传

另请参阅 is_uploaded_file()move_uploaded_file() 函数条目以获取更多信息。以下示例将处理来自表单的文件上传。

<?php
$uploaddir
= '/var/www/uploads/';
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);

echo
'<pre>';
if (
move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
echo
"File is valid, and was successfully uploaded.\n";
} else {
echo
"Possible file upload attack!\n";
}

echo
'Here is some more debugging info:';
print_r($_FILES);

print
"</pre>";

?>

接收上传文件的 PHP 脚本应实现确定如何处理上传文件所需的任何逻辑。例如,您可以使用 $_FILES['userfile']['size'] 变量丢弃任何过小或过大的文件。您可以使用 $_FILES['userfile']['type'] 变量丢弃任何不符合特定类型标准的文件,但仅将其作为一系列检查中的第一步,因为此值完全受客户端控制,并且在 PHP 端未经检查。此外,您可以使用 $_FILES['userfile']['error'] 并根据 错误代码 计划您的逻辑。无论逻辑如何,您都应从临时目录中删除文件或将其移动到其他位置。

如果在表单中未选择任何文件进行上传,则 PHP 将返回 $_FILES['userfile']['size'] 为 0,并将 $_FILES['userfile']['tmp_name'] 返回为 none。

如果文件未被移走或重命名,则在请求结束时将从临时目录中删除该文件。

示例 #3 上传文件数组

PHP 甚至支持文件中的 HTML 数组功能

<form action="" method="post" enctype="multipart/form-data">
<p>Pictures:
<input type="file" name="pictures[]" />
<input type="file" name="pictures[]" />
<input type="file" name="pictures[]" />
<input type="submit" value="Send" />
</p>
</form>
<?php
foreach ($_FILES["pictures"]["error"] as $key => $error) {
if (
$error == UPLOAD_ERR_OK) {
$tmp_name = $_FILES["pictures"]["tmp_name"][$key];
// basename() may prevent filesystem traversal attacks;
// further validation/sanitation of the filename may be appropriate
$name = basename($_FILES["pictures"]["name"][$key]);
move_uploaded_file($tmp_name, "data/$name");
}
}
?>

可以使用 会话上传进度 实现文件上传进度条。

添加注释

用户贡献的注释 8 条注释

daevid at daevid dot com
15 年前
我认为附件数组的工作方式有点笨拙。通常 PHP 开发人员都非常出色,但这确实违反直觉。它应该更像这样

数组
(
[0] => 数组
(
[name] => facepalm.jpg
[type] => image/jpeg
[tmp_name] => /tmp/phpn3FmFr
[error] => 0
[size] => 15476
)

[1] => 数组
(
[name] =>
[type] =>
[tmp_name] =>
[error] => 4
[size] =>
)
)

而不是这样
数组
(
[name] => 数组
(
[0] => facepalm.jpg
[1] =>
)

[type] => 数组
(
[0] => image/jpeg
[1] =>
)

[tmp_name] => 数组
(
[0] => /tmp/phpn3FmFr
[1] =>
)

[error] => 数组
(
[0] => 0
[1] => 4
)

[size] => 数组
(
[0] => 15476
[1] => 0
)
)

无论如何,这里有一个比上面文档中简略示例更完整的示例

<?php
foreach ($_FILES["attachment"]["error"] as $key => $error)
{
$tmp_name = $_FILES["attachment"]["tmp_name"][$key];
if (!
$tmp_name) continue;

$name = basename($_FILES["attachment"]["name"][$key]);

if (
$error == UPLOAD_ERR_OK)
{
if (
move_uploaded_file($tmp_name, "/tmp/".$name) )
$uploaded_array[] .= "已上传文件 '".$name."'.<br/>\n";
else
$errormsg .= "无法将上传的文件 '".$tmp_name."' 移动到 '".$name."'<br/>\n";
}
else
$errormsg .= "上传错误。[".$error."] 在文件 '".$name."'<br/>\n";
}
?>
mpyw
8 年前
不要使用 Coreywelch 或 Daevid 的方法,因为他们的方法只能处理二维结构。$_FILES 可以包含任何层次结构,例如 3D 或 4D 结构。

以下表单示例会破坏他们的代码

<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="files[x][y][z]">
<input type="submit">
</form>

作为解决方案,您应该使用基于 PSR-7 的 zendframework/zend-diactoros。

GitHub

https://github.com/zendframework/zend-diactoros

示例

<?php

use Psr\Http\Message\UploadedFileInterface;
use
Zend\Diactoros\ServerRequestFactory;

$request = ServerRequestFactory::fromGlobals();

if (
$request->getMethod() !== 'POST') {
http_response_code(405);
exit(
'请使用 POST 方法。');
}

$uploaded_files = $request->getUploadedFiles();

if (
!isset(
$uploaded_files['files']['x']['y']['z']) ||
!
$uploaded_files['files']['x']['y']['z'] instanceof UploadedFileInterface
) {
http_response_code(400);
exit(
'无效的请求体。');
}

$file = $uploaded_files['files']['x']['y']['z'];

if (
$file->getError() !== UPLOAD_ERR_OK) {
http_response_code(400);
exit(
'文件上传失败。');
}

$file->moveTo('/path/to/new/file');

?>
coreywelch+phpnet at gmail dot com
8 年前
文档中没有关于 HTML 数组功能如何格式化 $_FILES 数组的任何详细信息。

$_FILES 数组示例

对于单个文件 -

数组
(
[document] => 数组
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)
)

使用 HTML 数组功能的多文件 -

数组
(
[documents] => 数组
(
[name] => 数组
(
[0] => sample-file.doc
[1] => sample-file.doc
)

[type] => 数组
(
[0] => application/msword
[1] => application/msword
)

[tmp_name] => 数组
(
[0] => /tmp/path/phpVGCDAJ
[1] => /tmp/path/phpVGCDAJ
)

[error] => 数组
(
[0] => 0
[1] => 0
)

[size] => 数组
(
[0] => 0
[1] => 0
)

)

)

当您拥有一个同时使用单个文件和 HTML 数组功能的表单时,就会出现问题。数组未规范化,并且往往会使编写代码变得非常混乱。我已经包含了一种规范化 $_FILES 数组的好方法。

<?php

function normalize_files_array($files = []) {

$normalized_array = [];

foreach(
$files as $index => $file) {

if (!
is_array($file['name'])) {
$normalized_array[$index][] = $file;
continue;
}

foreach(
$file['name'] as $idx => $name) {
$normalized_array[$index][$idx] = [
'name' => $name,
'type' => $file['type'][$idx],
'tmp_name' => $file['tmp_name'][$idx],
'error' => $file['error'][$idx],
'size' => $file['size'][$idx]
];
}

}

return
$normalized_array;

}

?>

以下是上述方法的输出。

数组
(
[document] => 数组
(
[0] => 数组
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)

)

[documents] => 数组
(
[0] => 数组
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)

[1] => 数组
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)

)

)
fravadona at gmail dot com
4 年前
mpyw 是对的,PSR-7 非常棒,但对于简单的项目来说有点过头(在我看来)。

这是一个函数示例,它以(类似 PSR-7 的)规范化树的形式返回文件上传元数据。此函数处理任何维度的上传元数据。

我使代码极其简单,它不会验证 $_FILES 中的任何内容,等等... **最重要的是,它以** *未定义行为* **的方式调用 array_walk_recursive!!!**

您可以针对 PSR-7 规范的示例(https://www.php-fig.org/psr/psr-7/#16-uploaded-files)进行测试,并尝试添加您自己的检查以检测最后一个示例中的错误 ^^



<?php
/**
* 此代码绝对不适用于生产环境!!!希望其中的见解能帮助您!!!
*/
function getNormalizedFiles()
{
$normalized = array();

if ( isset(
$_FILES) ) {

foreach (
$_FILES as $field => $metadata ) {

$normalized[$field] = array(); // 为了 array_replace_recursive 需要进行初始化

foreach ( $metadata as $meta => $data ) { // $meta 为 'tmp_name'、'error' 等...

if ( is_array($data) ) {

// 将当前元数据插入到每个叶子节点之前!!!错误地使用了 ARRAY_WALK_RECURSIVE!!!
array_walk_recursive($data, function (&$v,$k) use ($meta) { $v = array( $meta => $v ); });

// 将当前元数据与之前的元数据合并
$normalized[$field] = array_replace_recursive($normalized[$field], $data);

} else {
$normalized[$field][$meta] = $data;
}
}
}
}
return
$normalized;
}
?>
anon
9年前
为了清楚起见;您不希望用以下脚本替换示例脚本的原因是
$uploaddir = './';
因为如果您没有对文件进行编码约束,那么一个技术宅就可以上传一个php脚本,其名称与您脚本目录中的某个脚本相同。

在正确的设置和权限下,php-cgi 甚至能够替换 php 文件。

想象一下,如果它替换了上传后处理器文件本身。下一次“上传”可能会导致一些简单的漏洞利用。

即使替换不可行;上传 .htaccess 文件也可能导致一些问题,尤其是在技术宅在其中插入一个恶意脚本以使用 htaccess 重定向到他的上传文件之后。

可能还有更多利用它的方法。不要让那些技术宅得逞。

更明智的做法是为上传文件使用一个新的目录,并使用某种唯一的命名算法;甚至可以创建一个 cron 作业来清理目录,以便旧文件不会停留太久。
eslindsey at gmail dot com
15 年前
另请注意,由于提交表单的浏览器提供了 MAX_FILE_SIZE 隐藏字段,因此客户端可以轻松地覆盖它。您应该始终在文件到达您之后对其进行自己的检查和错误检查,而不是依赖客户端提交的信息。这包括检查文件大小(始终检查实际数据长度与报告的文件大小)以及文件类型(浏览器提交的 MIME 类型充其量是不准确的,最坏的情况是故意设置为不正确的值)。
Mark
14年前
如果用户尝试上传的文件大小超过 php.ini 中的 post_max_size,则 $_FILES 将为空。

php.ini 中的 post_max_size 应该 >= upload_max_filesize。
claude dot pache at gmail dot com
15 年前
请注意,MAX_FILE_SIZE 隐藏字段仅由接收请求的 PHP 脚本用作指令,以拒绝大于给定界限的文件。此字段对浏览器没有意义,它不提供文件大小的客户端检查,并且与 Web 标准或浏览器功能无关。
To Top