Загрузка файла: проверить "tmp_name", или "error", или другой ключ?
Краткая версия:
Когда пользователь загружает файл, используя форму, массив сохраняется в глобальной переменной $_FILES
, Например, при использовании:
<input type="file" name="myfiles0" />
глобальная переменная выглядит так:
$_FILES = [
'myfiles0' => [
'name' => 'image-1.jpg',
'type' => 'image/jpeg',
'tmp_name' => '[path-to]/tmp/php/phptiV897',
'error' => 0,
'size' => 92738,
],
]
В принципе мне нужно знать, какой из ключей массива $_FILES['myfiles0']
всегда существует и (возможно) всегда установлен, независимо от того, как выглядят другие клавиши или какой браузер используется. Не могли бы вы сказать мне, пожалуйста?
Пожалуйста, примите во внимание, что $_FILES
переменная также может содержать многомерные массивы для файлов, загруженных с использованием записи массива, например:
<input type="file" name="myfiles1[demo][images][]" multiple />
Длинная версия:
Для реализации загруженных файлов PSR-7 мне нужно выполнить нормализацию списка загруженных файлов. Первоначальный список может быть предоставлен пользователем или может быть результатом стандартной загрузки файла с использованием формы, например $_FILES
глобальная переменная. Для процесса нормализации мне нужно проверить наличие и "правильность" (возможно, неудачный выбор слова) одного из следующих стандартных ключей загрузки файлов:
name
type
tmp_name
error
size
В принципе, если в представленном списке загруженных файлов (который также может быть многомерным массивом) выбран ключ (я выбрал tmp_name
пока), тогда предполагается, что элемент массива, к которому принадлежит ключ, является стандартным элементом массива загрузки файлов, содержащим приведенный выше список ключей. В противном случае, например, если выбранный ключ не найден, предполагается, что соответствующий элемент массива является экземпляром UploadedFileInterface.
К сожалению, в случае стандартной загрузки файла я нигде не могу найти отдельную информацию о том, какой ключ (из приведенного выше списка) всегда существует и (возможно) всегда установлен в $_FILES
переменная, независимо от того, как выглядят другие ключи списка или какой браузер используется.
Буду признателен, если вы поможете мне в этом вопросе.
Спасибо.
0 ответов
Я решил использовать tmp_name
ключ для проверки загрузки файла (ов).
К сожалению, я принял это решение очень давно. Так что я больше не могу вспомнить все аргументы, подтверждающие это, вытекающие из документации, которую я читал, и тестов, которые я провел. Хотя одним из аргументов было то, что, по сравнению с другими ключами, значениеtmp_name
ключ не может быть установлен / изменен на стороне клиента. Среда, в которой работает приложение, решает, какое значение должно быть установлено для него.
Я размещу здесь окончательную версию реализации PSR-7 и PSR-17 (в отношении загруженных файлов), которую я написал тогда. Может кому будет полезно.
Реализация ServerRequestFactoryInterface
:
Он читает список загруженных файлов (находится в $_FILES
или передается вручную в качестве аргумента) и, если это еще не сделано, преобразует его в "нормализованное дерево метаданных загрузки, каждый лист которого является экземпляром Psr\Http\Message\UploadedFileInterface" (см. "1.6 Загруженные файлы" в PSR-7).
Затем он создает ServerRequestInterface
например, передав ему нормализованный список загруженных файлов.
<?php
namespace MyLib\Http\Message\Factory\SapiServerRequestFactory;
use MyLib\Http\Message\Uri;
use MyLib\Http\Message\Stream;
use MyLib\Http\Message\UploadedFile;
use MyLib\Http\Message\ServerRequest;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\ServerRequestInterface;
use MyLib\Http\Message\Factory\ServerRequestFactory;
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
/**
* Server request factory for the "apache2handler" SAPI.
*/
class Apache2HandlerFactory extends ServerRequestFactory {
/**
* Create a new server request by seeding the generated request
* instance with the elements of the given array of SAPI parameters.
*
* @param array $serverParams (optional) Array of SAPI parameters with which to seed
* the generated request instance.
* @return ServerRequestInterface The new server request.
*/
public function createServerRequestFromArray(array $serverParams = []): ServerRequestInterface {
if (!$serverParams) {
$serverParams = $_SERVER;
}
$this->headers = $this->buildHeaders($serverParams);
$method = $this->buildMethod($serverParams);
$uri = $this->buildUri($serverParams, $this->headers);
$this->parsedBody = $this->buildParsedBody($this->parsedBody, $method, $this->headers);
$this->queryParams = $this->queryParams ?: $_GET;
$this->uploadedFiles = $this->buildUploadedFiles($this->uploadedFiles ?: $_FILES);
$this->cookieParams = $this->buildCookieParams($this->headers, $this->cookieParams);
$this->protocolVersion = $this->buildProtocolVersion($serverParams, $this->protocolVersion);
return parent::createServerRequest($method, $uri, $serverParams);
}
/*
* Custom methods.
*/
// [... All other methods ...]
/**
* Build the list of uploaded files as a normalized tree of upload metadata,
* with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
*
* Not part of PSR-17.
*
* @param array $uploadedFiles The list of uploaded files (normalized or not).
* Data MAY come from $_FILES or the message body.
* @return array A tree of upload files in a normalized structure, with each leaf
* an instance of UploadedFileInterface.
*/
private function buildUploadedFiles(array $uploadedFiles) {
return $this->normalizeUploadedFiles($uploadedFiles);
}
/**
* Normalize - if not already - the list of uploaded files as a tree of upload
* metadata, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
*
* Not part of PSR-17.
*
* IMPORTANT: For a correct normalization of the uploaded files list, the FIRST OCCURRENCE
* of the key "tmp_name" is checked against. See "POST method uploads" link.
* As soon as the key will be found in an item of the uploaded files list, it
* will be supposed that the array item to which it belongs is an array with
* a structure similar to the one saved in the global variable $_FILES when a
* standard file upload is executed.
*
* @link https://secure.php.net/manual/en/features.file-upload.post-method.php POST method uploads.
* @link https://secure.php.net/manual/en/reserved.variables.files.php $_FILES.
* @link https://tools.ietf.org/html/rfc1867 Form-based File Upload in HTML.
* @link https://tools.ietf.org/html/rfc2854 The 'text/html' Media Type.
*
* @param array $uploadedFiles The list of uploaded files (normalized or not). Data MAY come
* from $_FILES or the message body.
* @return array A tree of upload files in a normalized structure, with each leaf
* an instance of UploadedFileInterface.
* @throws \InvalidArgumentException An invalid structure of uploaded files list is provided.
*/
private function normalizeUploadedFiles(array $uploadedFiles) {
$normalizedUploadedFiles = [];
foreach ($uploadedFiles as $key => $item) {
if (is_array($item)) {
$normalizedUploadedFiles[$key] = array_key_exists('tmp_name', $item) ?
$this->normalizeFileUploadItem($item) :
$this->normalizeUploadedFiles($item);
} elseif ($item instanceof UploadedFileInterface) {
$normalizedUploadedFiles[$key] = $item;
} else {
throw new \InvalidArgumentException(
'The structure of the uploaded files list is not valid.'
);
}
}
return $normalizedUploadedFiles;
}
/**
* Normalize the file upload item which contains the FIRST OCCURRENCE of the key "tmp_name".
*
* This method returns a tree structure, with each leaf
* an instance of Psr\Http\Message\UploadedFileInterface.
*
* Not part of PSR-17.
*
* @param array $item The file upload item.
* @return array The file upload item as a tree structure, with each leaf
* an instance of UploadedFileInterface.
* @throws \InvalidArgumentException The value at the key "tmp_name" is empty.
*/
private function normalizeFileUploadItem(array $item) {
// Validate the value at the key "tmp_name".
if (empty($item['tmp_name'])) {
throw new \InvalidArgumentException(
'The value of the key "tmp_name" in the uploaded files list '
. 'must be a non-empty value or a non-empty array.'
);
}
// Get the value at the key "tmp_name".
$filename = $item['tmp_name'];
// Return the normalized value at the key "tmp_name".
if (is_array($filename)) {
return $this->normalizeFileUploadTmpNameItem($filename, $item);
}
// Get the leaf values.
$size = $item['size'] ?? null;
$error = $item['error'] ?? \UPLOAD_ERR_OK;
$clientFilename = $item['name'] ?? null;
$clientMediaType = $item['type'] ?? null;
// Return an instance of UploadedFileInterface.
return $this->createUploadedFile(
$filename
, $size
, $error
, $clientFilename
, $clientMediaType
);
}
/**
* Normalize the array assigned as value to the FIRST OCCURRENCE of the key "tmp_name" in a
* file upload item of the uploaded files list. It is recursively iterated, in order to build
* a tree structure, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
*
* Not part of PSR-17.
*
* @param array $item The array assigned as value to the FIRST OCCURRENCE of the key "tmp_name".
* @param array $currentElements An array holding the file upload key/value pairs
* of the current item.
* @return array A tree structure, with each leaf an instance of UploadedFileInterface.
* @throws \InvalidArgumentException
*/
private function normalizeFileUploadTmpNameItem(array $item, array $currentElements) {
$normalizedItem = [];
foreach ($item as $key => $value) {
if (is_array($value)) {
// Validate the values at the keys "size" and "error".
if (
!isset($currentElements['size'][$key]) ||
!is_array($currentElements['size'][$key]) ||
!isset($currentElements['error'][$key]) ||
!is_array($currentElements['error'][$key])
) {
throw new \InvalidArgumentException(
'The structure of the items assigned to the keys "size" and "error" '
. 'in the uploaded files list must be identical with the one of the '
. 'item assigned to the key "tmp_name". This restriction does not '
. 'apply to the leaf elements.'
);
}
// Get the array values.
$filename = $currentElements['tmp_name'][$key];
$size = $currentElements['size'][$key];
$error = $currentElements['error'][$key];
$clientFilename = isset($currentElements['name'][$key]) &&
is_array($currentElements['name'][$key]) ?
$currentElements['name'][$key] :
null;
$clientMediaType = isset($currentElements['type'][$key]) &&
is_array($currentElements['type'][$key]) ?
$currentElements['type'][$key] :
null;
// Normalize recursively.
$normalizedItem[$key] = $this->normalizeFileUploadTmpNameItem($value, [
'tmp_name' => $filename,
'size' => $size,
'error' => $error,
'name' => $clientFilename,
'type' => $clientMediaType,
]);
} else {
// Get the leaf values.
$filename = $currentElements['tmp_name'][$key];
$size = $currentElements['size'][$key] ?? null;
$error = $currentElements['error'][$key] ?? \UPLOAD_ERR_OK;
$clientFilename = $currentElements['name'][$key] ?? null;
$clientMediaType = $currentElements['type'][$key] ?? null;
// Create an instance of UploadedFileInterface.
$normalizedItem[$key] = $this->createUploadedFile(
$filename
, $size
, $error
, $clientFilename
, $clientMediaType
);
}
}
return $normalizedItem;
}
/**
* Create an instance of UploadedFileInterface.
*
* Not part of PSR-17.
*
* @param string $filename The filename of the uploaded file.
* @param int|null $size (optional) The file size in bytes or null if unknown.
* @param int $error (optional) The error associated with the uploaded file. The value MUST be
* one of PHP's UPLOAD_ERR_XXX constants.
* @param string|null $clientFilename (optional) The filename sent by the client, if any.
* @param string|null $clientMediaType (optional) The media type sent by the client, if any.
* @return UploadedFileInterface
*/
private function createUploadedFile(
string $filename
, int $size = null
, int $error = \UPLOAD_ERR_OK
, string $clientFilename = null
, string $clientMediaType = null
): UploadedFileInterface {
// Create a stream with read-only access.
$stream = new Stream($filename, 'rb');
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
}
}
Базовый класс ServerRequestFactory
:
<?php
namespace MyLib\Http\Message\Factory;
use MyLib\Http\Message\Uri;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\StreamInterface;
use MyLib\Http\Message\ServerRequest;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
/**
* Server request factory.
*/
class ServerRequestFactory implements ServerRequestFactoryInterface {
/**
* Message body.
*
* @var StreamInterface
*/
protected $body;
/**
* Attributes list.
*
* @var array
*/
protected $attributes = [];
/**
* Headers list with case-insensitive header names.
* A header value can be a string, or an array of strings.
*
* [
* 'header-name 1' => 'header-value',
* 'header-name 2' => [
* 'header-value 1',
* 'header-value 2',
* ],
* ]
*
* @link https://tools.ietf.org/html/rfc7230#section-3.2 Header Fields.
* @link https://tools.ietf.org/html/rfc7231#section-5 Request Header Fields.
*
* @var array
*/
protected $headers = [];
/**
* Parsed body, e.g. the deserialized body parameters, if any.
*
* @var null|array|object
*/
protected $parsedBody;
/**
* Query string arguments.
*
* @var array
*/
protected $queryParams = [];
/**
* Uploaded files.
*
* @var array
*/
protected $uploadedFiles = [];
/**
* Cookies.
*
* @var array
*/
protected $cookieParams = [];
/**
* HTTP protocol version.
*
* @var string
*/
protected $protocolVersion;
/**
*
* @param StreamInterface $body Message body.
* @param array $attributes (optional) Attributes list.
* @param array $headers (optional) Headers list with case-insensitive header names.
* A header value can be a string, or an array of strings.
* @param null|array|object $parsedBody (optional) Parsed body, e.g. the deserialized body
* parameters, if any. The data IS NOT REQUIRED to come from $_POST, but MUST be the
* results of deserializing the request body content.
* @param array $queryParams (optional) Query string arguments. They MAY be injected from
* PHP's $_GET superglobal, or MAY be derived from some other value such as the URI.
* @param array $uploadedFiles (optional) Uploaded files list as a normalized tree of upload
* metadata, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
* @param array $cookieParams (optional) Cookies. The data IS NOT REQUIRED to come from
* the $_COOKIE superglobal, but MUST be compatible with the structure of $_COOKIE.
* @param string $protocolVersion (optional) HTTP protocol version.
*/
public function __construct(
StreamInterface $body
, array $attributes = []
, array $headers = []
, $parsedBody = null
, array $queryParams = []
, array $uploadedFiles = []
, array $cookieParams = []
, string $protocolVersion = '1.1'
) {
$this->body = $body;
$this->attributes = $attributes;
$this->headers = $headers;
$this->parsedBody = $parsedBody;
$this->queryParams = $queryParams;
$this->uploadedFiles = $uploadedFiles;
$this->cookieParams = $cookieParams;
$this->protocolVersion = $protocolVersion;
}
/**
* Create a new server request.
*
* Note that server-params are taken precisely as given - no parsing/processing
* of the given values is performed, and, in particular, no attempt is made to
* determine the HTTP method or URI, which must be provided explicitly.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request. If
* the value is a string, the factory MUST create a UriInterface
* instance based on it.
* @param array $serverParams Array of SAPI parameters with which to seed
* the generated request instance.
*
* @return ServerRequestInterface
*/
public function createServerRequest(
string $method
, $uri
, array $serverParams = []
): ServerRequestInterface {
// Validate method and URI.
$this
->validateMethod($method)
->validateUri($uri)
;
// Create an instance of UriInterface.
if (is_string($uri)) {
$uri = new Uri($uri);
}
// Create the server request.
return new ServerRequest(
$method
, $uri
, $this->body
, $this->attributes
, $this->headers
, $serverParams
, $this->parsedBody
, $this->queryParams
, $this->uploadedFiles
, $this->cookieParams
, $this->protocolVersion
);
}
// [... Other methods ...]
}
Создание ServerRequestInterface
экземпляр ServerRequestFactoryInterface
реализация:
<?php
use MyLib\Http\Message\Factory\SapiServerRequestFactory\Apache2HandlerFactory;
// [...]
// Create stream with read-only access.
$body = $streamFactory->createStreamFromFile('php://temp', 'rb');
$serverRequestFactory = new Apache2HandlerFactory(
$body
, [] /* attributes */
, [] /* headers */
, $_POST /* parsed body */
, $_GET /* query params */
, $_FILES /* uploaded files */
, $_COOKIE /* cookie params */
, '1.1' /* http protocol version */
);
$serverRequest = $serverRequestFactory->createServerRequestFromArray($_SERVER);
// [...]