我决定使用tmp_name
密钥进行文件上传验证。
不幸的是,我很久以前就做出了这个决定。所以我不记得所有支持它的论点了,这是我阅读的文档和我执行的测试的结果。但是,其中一个论点是,与其他键相比,键的值tmp_name
不能在客户端设置/更改。应用程序运行的环境决定了应该为其设置哪个值。
我将在此处发布我当时编写的PSR-7和PSR-17实现的最终版本(关于上传的文件)。也许它会对某人有所帮助。
的实施ServerRequestFactoryInterface
:
它读取上传文件的列表(在 中找到$_FILES
,或作为参数手动传递),如果尚未完成,则将其转换为“上传元数据的规范化树,每个叶子都是 Psr\Http\Message\UploadedFileInterface 的实例”(请参阅PSR-7中的“1.6 上传的文件”)。
然后它创建一个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);
// [...]