PHP 日本研討會 2024

POST 方法上傳

此功能允許使用者上傳文字和二進位檔案。透過 PHP 的驗證和檔案處理函式,您可以完全控制允許誰上傳,以及上傳檔案後要執行的操作。

PHP 能夠接收來自任何符合 RFC-1867 標準瀏覽器的檔案上傳。

注意相關組態注意事項

另請參閱 php.ini 中的 file_uploadsupload_max_filesizeupload_tmp_dirpost_max_sizemax_input_time 指令。

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 檔案。

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
"檔案有效,且已成功上傳。\n";
} else {
echo
"可能的檔案上傳攻擊!\n";
}

echo
'以下是一些更多的除錯資訊:';
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() 可能可以防止檔案系統遍歷攻擊;
// 進一步驗證/清理檔案名稱可能是適當的
$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[] .= "Uploaded file '".$name."'.<br/>\n";
else
$errormsg .= "Could not move uploaded file '".$tmp_name."' to '".$name."'<br/>\n";
}
else
$errormsg .= "Upload error. [".$error."] on file '".$name."'<br/>\n";
}
?>
mpyw
8 年前
請不要使用 Coreywelch 或 Daevid 的方法,因為他們的方法只能處理二維結構。$_FILES 可以包含任何層次結構,例如三維或四維結構。

以下表單範例會破壞他們的程式碼

<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(
'Use POST method.');
}

$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(
'Invalid request body.');
}

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

if (
$file->getError() !== UPLOAD_ERR_OK) {
http_response_code(400);
exit(
'File uploading failed.');
}

$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) ) {

// 在每個葉節點前插入目前的 meta !!! 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 job 來清理該目錄,讓舊檔案不會停留太久。
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