[ * 'request' => [ * 'parsers' => [ * 'multipart/form-data' => 'yii\web\MultipartFormDataParser' * ], * ], * // ... * ], * // ... * ]; * ``` * * Method [[parse()]] of this parser automatically populates `$_FILES` with the files parsed from raw body. * * > Note: since this is a request parser, it will initialize `$_FILES` values on [[Request::getBodyParams()]]. * Until this method is invoked, `$_FILES` array will remain empty even if there are submitted files in the * request body. Make sure you have requested body params before any attempt to get uploaded file in case * you are using this parser. * * Usage example: * * ```php * use yii\web\UploadedFile; * * $restRequestData = Yii::$app->request->getBodyParams(); * $uploadedFile = UploadedFile::getInstancesByName('photo'); * * $model = new Item(); * $model->populate($restRequestData); * copy($uploadedFile->tempName, '/path/to/file/storage/photo.jpg'); * ``` * * > Note: although this parser fully emulates regular structure of the `$_FILES`, related temporary * files, which are available via `tmp_name` key, will not be recognized by PHP as uploaded ones. * Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them. * * @property int $uploadFileMaxCount Maximum upload files count. * @property int $uploadFileMaxSize Upload file max size in bytes. * * @author Paul Klimov * @since 2.0.10 */ class MultipartFormDataParser extends BaseObject implements RequestParserInterface { /** * @var bool whether to parse raw body even for 'POST' request and `$_FILES` already populated. * By default this option is disabled saving performance for 'POST' requests, which are already * processed by PHP automatically. * > Note: if this option is enabled, value of `$_FILES` will be reset on each parse. * @since 2.0.13 */ public $force = false; /** * @var int upload file max size in bytes. */ private $_uploadFileMaxSize; /** * @var int maximum upload files count. */ private $_uploadFileMaxCount; /** * @return int upload file max size in bytes. */ public function getUploadFileMaxSize() { if ($this->_uploadFileMaxSize === null) { $this->_uploadFileMaxSize = $this->getByteSize(ini_get('upload_max_filesize')); } return $this->_uploadFileMaxSize; } /** * @param int $uploadFileMaxSize upload file max size in bytes. */ public function setUploadFileMaxSize($uploadFileMaxSize) { $this->_uploadFileMaxSize = $uploadFileMaxSize; } /** * @return int maximum upload files count. */ public function getUploadFileMaxCount() { if ($this->_uploadFileMaxCount === null) { $this->_uploadFileMaxCount = (int)ini_get('max_file_uploads'); } return $this->_uploadFileMaxCount; } /** * @param int $uploadFileMaxCount maximum upload files count. */ public function setUploadFileMaxCount($uploadFileMaxCount) { $this->_uploadFileMaxCount = $uploadFileMaxCount; } /** * {@inheritdoc} */ public function parse($rawBody, $contentType) { if (!$this->force) { if (!empty($_POST) || !empty($_FILES)) { // normal POST request is parsed by PHP automatically return $_POST; } } else { $_FILES = []; } if (empty($rawBody)) { return []; } if (!preg_match('/boundary="?(.*)"?$/is', $contentType, $matches)) { return []; } $boundary = trim($matches[1], '"'); $bodyParts = preg_split('/\\R?-+' . preg_quote($boundary, '/') . '/s', $rawBody); array_pop($bodyParts); // last block always has no data, contains boundary ending like `--` $bodyParams = []; $filesCount = 0; foreach ($bodyParts as $bodyPart) { if (empty($bodyPart)) { continue; } list($headers, $value) = preg_split('/\\R\\R/', $bodyPart, 2); $headers = $this->parseHeaders($headers); if (!isset($headers['content-disposition']['name'])) { continue; } if (isset($headers['content-disposition']['filename'])) { // file upload: if ($filesCount >= $this->getUploadFileMaxCount()) { continue; } $fileInfo = [ 'name' => $headers['content-disposition']['filename'], 'type' => ArrayHelper::getValue($headers, 'content-type', 'application/octet-stream'), 'size' => StringHelper::byteLength($value), 'error' => UPLOAD_ERR_OK, 'tmp_name' => null, ]; if ($fileInfo['size'] > $this->getUploadFileMaxSize()) { $fileInfo['error'] = UPLOAD_ERR_INI_SIZE; } else { $tmpResource = tmpfile(); if ($tmpResource === false) { $fileInfo['error'] = UPLOAD_ERR_CANT_WRITE; } else { $tmpResourceMetaData = stream_get_meta_data($tmpResource); $tmpFileName = $tmpResourceMetaData['uri']; if (empty($tmpFileName)) { $fileInfo['error'] = UPLOAD_ERR_CANT_WRITE; @fclose($tmpResource); } else { fwrite($tmpResource, $value); rewind($tmpResource); $fileInfo['tmp_name'] = $tmpFileName; $fileInfo['tmp_resource'] = $tmpResource; // save file resource, otherwise it will be deleted } } } $this->addFile($_FILES, $headers['content-disposition']['name'], $fileInfo); $filesCount++; } else { // regular parameter: $this->addValue($bodyParams, $headers['content-disposition']['name'], $value); } } return $bodyParams; } /** * Parses content part headers. * @param string $headerContent headers source content * @return array parsed headers. */ private function parseHeaders($headerContent) { $headers = []; $headerParts = preg_split('/\\R/su', $headerContent, -1, PREG_SPLIT_NO_EMPTY); foreach ($headerParts as $headerPart) { if (strpos($headerPart, ':') === false) { continue; } list($headerName, $headerValue) = explode(':', $headerPart, 2); $headerName = strtolower(trim($headerName)); $headerValue = trim($headerValue); if (strpos($headerValue, ';') === false) { $headers[$headerName] = $headerValue; } else { $headers[$headerName] = []; foreach (explode(';', $headerValue) as $part) { $part = trim($part); if (strpos($part, '=') === false) { $headers[$headerName][] = $part; } else { list($name, $value) = explode('=', $part, 2); $name = strtolower(trim($name)); $value = trim(trim($value), '"'); $headers[$headerName][$name] = $value; } } } } return $headers; } /** * Adds value to the array by input name, e.g. `Item[name]`. * @param array $array array which should store value. * @param string $name input name specification. * @param mixed $value value to be added. */ private function addValue(&$array, $name, $value) { $nameParts = preg_split('/\\]\\[|\\[/s', $name); $current = &$array; foreach ($nameParts as $namePart) { $namePart = trim($namePart, ']'); if ($namePart === '') { $current[] = []; $keys = array_keys($current); $lastKey = array_pop($keys); $current = &$current[$lastKey]; } else { if (!isset($current[$namePart])) { $current[$namePart] = []; } $current = &$current[$namePart]; } } $current = $value; } /** * Adds file info to the uploaded files array by input name, e.g. `Item[file]`. * @param array $files array containing uploaded files * @param string $name input name specification. * @param array $info file info. */ private function addFile(&$files, $name, $info) { if (strpos($name, '[') === false) { $files[$name] = $info; return; } $fileInfoAttributes = [ 'name', 'type', 'size', 'error', 'tmp_name', 'tmp_resource', ]; $nameParts = preg_split('/\\]\\[|\\[/s', $name); $baseName = array_shift($nameParts); if (!isset($files[$baseName])) { $files[$baseName] = []; foreach ($fileInfoAttributes as $attribute) { $files[$baseName][$attribute] = []; } } else { foreach ($fileInfoAttributes as $attribute) { $files[$baseName][$attribute] = (array) $files[$baseName][$attribute]; } } foreach ($fileInfoAttributes as $attribute) { if (!isset($info[$attribute])) { continue; } $current = &$files[$baseName][$attribute]; foreach ($nameParts as $namePart) { $namePart = trim($namePart, ']'); if ($namePart === '') { $current[] = []; $keys = array_keys($current); $lastKey = array_pop($keys); $current = &$current[$lastKey]; } else { if (!isset($current[$namePart])) { $current[$namePart] = []; } $current = &$current[$namePart]; } } $current = $info[$attribute]; } } /** * Gets the size in bytes from verbose size representation. * * For example: '5K' => 5*1024. * @param string $verboseSize verbose size representation. * @return int actual size in bytes. */ private function getByteSize($verboseSize) { if (empty($verboseSize)) { return 0; } if (is_numeric($verboseSize)) { return (int) $verboseSize; } $sizeUnit = trim($verboseSize, '0123456789'); $size = trim(str_replace($sizeUnit, '', $verboseSize)); if (!is_numeric($size)) { return 0; } switch (strtolower($sizeUnit)) { case 'kb': case 'k': return $size * 1024; case 'mb': case 'm': return $size * 1024 * 1024; case 'gb': case 'g': return $size * 1024 * 1024 * 1024; default: return 0; } } }