The short version:
When a user uploads a file using a form, an array is saved in the global variable $_FILES
. For example, when using:
<input type="file" name="myfiles0" />
the global variable looks like this:
$_FILES = [ 'myfiles0' => [ 'name' => 'image-1.jpg', 'type' => 'image/jpeg', 'tmp_name' => '[path-to]/tmp/php/phptiV897', 'error' => 0, 'size' => 92738, ], ]
In principle, I need to know which of the keys of the array $_FILES['myfiles0']
always exists and (maybe) is always set, no matter how the other keys look like, or which browser is used. Could you please tell me?
Please take into the consideration, that the $_FILES
variable can also contain multi-dimensional arrays for files uploaded using an array notation, like this:
<input type="file" name="myfiles1[demo][images][]" multiple />
The long version:
For my implemention of PSR-7 Uploaded files I need to do the normalization of the uploaded files list. The initial list can be provided by the user, or can be the result of a standard file upload using a form, e.g. the $_FILES
global variable. For the normalization process I need to check for the existence and “correctness” (maybe a poor choice of the word) of one of the following standard file upload keys:
name
type
tmp_name
error
size
In principle, if, in the provided uploaded files list (which can be a multi-dimensional array as well), the chosen key (I choosed tmp_name
for now) is found, then it will be supposed that the array item to which the key belongs is a standard file upload array item, containing the above key list. Otherwise, e.g. if the chosen key is not found, it will be supposed that the corresponding array item is an instance of UploadedFileInterface.
Unfortunately, in case of a standard file upload, I can’t find nowhere a solide information about which key (from the above list) always exists and (maybe) is always set in the $_FILES
variable, no matter how the other list keys look like, or which browser is used.
I would appreciate, if you could help me in this matter.
Thank you.
Advertisement
Answer
I decided to use the tmp_name
key for the file(s) upload validation.
Unfortunately I’ve taken this decision a long time ago. So I can’t remember anymore all arguments supporting it, resulted from the documentations I read and the tests I performed. Though, one of the arguments has been, that, in comparison with other key(s), the value of the tmp_name
key can’t be set/changed on the client-side. The environment on which the application is running decides which value should be set for it.
I’ll post here the final version of the PSR-7 & PSR-17 implementation (regarding uploaded files) that I wrote back then. Maybe it will be helpful for someone.
The implementation of ServerRequestFactoryInterface
:
It reads the list of uploaded files (found in $_FILES
, or manually passed as argument) and, if not already done, transforms it to “a normalized tree of upload metadata, with each leaf an instance of PsrHttpMessageUploadedFileInterface” (see “1.6 Uploaded files” in PSR-7).
Then it creates a ServerRequestInterface
instance, passing it the normalized list of uploaded files.
<?php namespace MyLibHttpMessageFactorySapiServerRequestFactory; use MyLibHttpMessageUri; use MyLibHttpMessageStream; use MyLibHttpMessageUploadedFile; use MyLibHttpMessageServerRequest; use PsrHttpMessageUploadedFileInterface; use PsrHttpMessageServerRequestInterface; use MyLibHttpMessageFactoryServerRequestFactory; use FigHttpMessageRequestMethodInterface 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 PsrHttpMessageUploadedFileInterface. * * 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 PsrHttpMessageUploadedFileInterface. * * 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 PsrHttpMessageUploadedFileInterface. * * 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 PsrHttpMessageUploadedFileInterface. * * 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); } }
The base class ServerRequestFactory
:
<?php namespace MyLibHttpMessageFactory; use MyLibHttpMessageUri; use PsrHttpMessageUriInterface; use PsrHttpMessageStreamInterface; use MyLibHttpMessageServerRequest; use PsrHttpMessageServerRequestInterface; use PsrHttpMessageServerRequestFactoryInterface; /** * 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 PsrHttpMessageUploadedFileInterface. * @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 ...] }
Creating the ServerRequestInterface
instance by the ServerRequestFactoryInterface
implementation:
<?php use MyLibHttpMessageFactorySapiServerRequestFactoryApache2HandlerFactory; // [...] // 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); // [...]