| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 | <?php/** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */namespace yii\web;use yii\base\BaseObject;use yii\helpers\ArrayHelper;use yii\helpers\StringHelper;/** * MultipartFormDataParser parses content encoded as 'multipart/form-data'. * This parser provides the fallback for the 'multipart/form-data' processing on non POST requests, * for example: the one with 'PUT' request method. * * In order to enable this parser you should configure [[Request::parsers]] in the following way: * * ```php * return [ *     'components' => [ *         '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. This also * means [[UploadedFile::saveAs()]] will fail as well. * * @property int $uploadFileMaxCount Maximum upload files count. * @property int $uploadFileMaxSize Upload file max size in bytes. * * @author Paul Klimov <klimov.paul@gmail.com> * @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 = 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 = $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);                            $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/s', $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;        }    }}
 |