You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

354 line
11KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\web;
  8. use yii\base\Object;
  9. use yii\helpers\ArrayHelper;
  10. use yii\helpers\StringHelper;
  11. /**
  12. * MultipartFormDataParser parses content encoded as 'multipart/form-data'.
  13. * This parser provides the fallback for the 'multipart/form-data' processing on non POST requests,
  14. * for example: the one with 'PUT' request method.
  15. *
  16. * In order to enable this parser you should configure [[Request::parsers]] in the following way:
  17. *
  18. * ```php
  19. * return [
  20. * 'components' => [
  21. * 'request' => [
  22. * 'parsers' => [
  23. * 'multipart/form-data' => 'yii\web\MultipartFormDataParser'
  24. * ],
  25. * ],
  26. * // ...
  27. * ],
  28. * // ...
  29. * ];
  30. * ```
  31. *
  32. * Method [[parse()]] of this parser automatically populates `$_FILES` with the files parsed from raw body.
  33. *
  34. * > Note: since this is a request parser, it will initialize `$_FILES` values on [[Request::getBodyParams()]].
  35. * Until this method is invoked, `$_FILES` array will remain empty even if there are submitted files in the
  36. * request body. Make sure you have requested body params before any attempt to get uploaded file in case
  37. * you are using this parser.
  38. *
  39. * Usage example:
  40. *
  41. * ```php
  42. * use yii\web\UploadedFile;
  43. *
  44. * $restRequestData = Yii::$app->request->getBodyParams();
  45. * $uploadedFile = UploadedFile::getInstancesByName('photo');
  46. *
  47. * $model = new Item();
  48. * $model->populate($restRequestData);
  49. * copy($uploadedFile->tempName, '/path/to/file/storage/photo.jpg');
  50. * ```
  51. *
  52. * > Note: although this parser fully emulates regular structure of the `$_FILES`, related temporary
  53. * files, which are available via `tmp_name` key, will not be recognized by PHP as uploaded ones.
  54. * Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them. This also
  55. * means [[UploadedFile::saveAs()]] will fail as well.
  56. *
  57. * @property integer $uploadFileMaxCount Maximum upload files count.
  58. * @property integer $uploadFileMaxSize Upload file max size in bytes.
  59. *
  60. * @author Paul Klimov <klimov.paul@gmail.com>
  61. * @since 2.0.10
  62. */
  63. class MultipartFormDataParser extends Object implements RequestParserInterface
  64. {
  65. /**
  66. * @var integer upload file max size in bytes.
  67. */
  68. private $_uploadFileMaxSize;
  69. /**
  70. * @var integer maximum upload files count.
  71. */
  72. private $_uploadFileMaxCount;
  73. /**
  74. * @return integer upload file max size in bytes.
  75. */
  76. public function getUploadFileMaxSize()
  77. {
  78. if ($this->_uploadFileMaxSize === null) {
  79. $this->_uploadFileMaxSize = $this->getByteSize(ini_get('upload_max_filesize'));
  80. }
  81. return $this->_uploadFileMaxSize;
  82. }
  83. /**
  84. * @param integer $uploadFileMaxSize upload file max size in bytes.
  85. */
  86. public function setUploadFileMaxSize($uploadFileMaxSize)
  87. {
  88. $this->_uploadFileMaxSize = $uploadFileMaxSize;
  89. }
  90. /**
  91. * @return integer maximum upload files count.
  92. */
  93. public function getUploadFileMaxCount()
  94. {
  95. if ($this->_uploadFileMaxCount === null) {
  96. $this->_uploadFileMaxCount = ini_get('max_file_uploads');
  97. }
  98. return $this->_uploadFileMaxCount;
  99. }
  100. /**
  101. * @param integer $uploadFileMaxCount maximum upload files count.
  102. */
  103. public function setUploadFileMaxCount($uploadFileMaxCount)
  104. {
  105. $this->_uploadFileMaxCount = $uploadFileMaxCount;
  106. }
  107. /**
  108. * @inheritdoc
  109. */
  110. public function parse($rawBody, $contentType)
  111. {
  112. if (!empty($_POST) || !empty($_FILES)) {
  113. // normal POST request is parsed by PHP automatically
  114. return $_POST;
  115. }
  116. if (empty($rawBody)) {
  117. return [];
  118. }
  119. if (!preg_match('/boundary=(.*)$/is', $contentType, $matches)) {
  120. return [];
  121. }
  122. $boundary = $matches[1];
  123. $bodyParts = preg_split('/-+' . preg_quote($boundary) . '/s', $rawBody);
  124. array_pop($bodyParts); // last block always has no data, contains boundary ending like `--`
  125. $bodyParams = [];
  126. $filesCount = 0;
  127. foreach ($bodyParts as $bodyPart) {
  128. if (empty($bodyPart)) {
  129. continue;
  130. }
  131. list($headers, $value) = preg_split("/\\R\\R/", $bodyPart, 2);
  132. $headers = $this->parseHeaders($headers);
  133. if (!isset($headers['content-disposition']['name'])) {
  134. continue;
  135. }
  136. if (isset($headers['content-disposition']['filename'])) {
  137. // file upload:
  138. if ($filesCount >= $this->getUploadFileMaxCount()) {
  139. continue;
  140. }
  141. $fileInfo = [
  142. 'name' => $headers['content-disposition']['filename'],
  143. 'type' => ArrayHelper::getValue($headers, 'content-type', 'application/octet-stream'),
  144. 'size' => StringHelper::byteLength($value),
  145. 'error' => UPLOAD_ERR_OK,
  146. 'tmp_name' => null,
  147. ];
  148. if ($fileInfo['size'] > $this->getUploadFileMaxSize()) {
  149. $fileInfo['error'] = UPLOAD_ERR_INI_SIZE;
  150. } else {
  151. $tmpResource = tmpfile();
  152. if ($tmpResource === false) {
  153. $fileInfo['error'] = UPLOAD_ERR_CANT_WRITE;
  154. } else {
  155. $tmpResourceMetaData = stream_get_meta_data($tmpResource);
  156. $tmpFileName = $tmpResourceMetaData['uri'];
  157. if (empty($tmpFileName)) {
  158. $fileInfo['error'] = UPLOAD_ERR_CANT_WRITE;
  159. @fclose($tmpResource);
  160. } else {
  161. fwrite($tmpResource, $value);
  162. $fileInfo['tmp_name'] = $tmpFileName;
  163. $fileInfo['tmp_resource'] = $tmpResource; // save file resource, otherwise it will be deleted
  164. }
  165. }
  166. }
  167. $this->addFile($_FILES, $headers['content-disposition']['name'], $fileInfo);
  168. $filesCount++;
  169. } else {
  170. // regular parameter:
  171. $this->addValue($bodyParams, $headers['content-disposition']['name'], $value);
  172. }
  173. }
  174. return $bodyParams;
  175. }
  176. /**
  177. * Parses content part headers.
  178. * @param string $headerContent headers source content
  179. * @return array parsed headers.
  180. */
  181. private function parseHeaders($headerContent)
  182. {
  183. $headers = [];
  184. $headerParts = preg_split("/\\R/s", $headerContent, -1, PREG_SPLIT_NO_EMPTY);
  185. foreach ($headerParts as $headerPart) {
  186. if (($separatorPos = strpos($headerPart, ':')) === false) {
  187. continue;
  188. }
  189. list($headerName, $headerValue) = explode(':', $headerPart, 2);
  190. $headerName = strtolower(trim($headerName));
  191. $headerValue = trim($headerValue);
  192. if (strpos($headerValue, ';') === false) {
  193. $headers[$headerName] = $headerValue;
  194. } else {
  195. $headers[$headerName] = [];
  196. foreach (explode(';', $headerValue) as $part) {
  197. $part = trim($part);
  198. if (strpos($part, '=') === false) {
  199. $headers[$headerName][] = $part;
  200. } else {
  201. list($name, $value) = explode('=', $part, 2);
  202. $name = strtolower(trim($name));
  203. $value = trim(trim($value), '"');
  204. $headers[$headerName][$name] = $value;
  205. }
  206. }
  207. }
  208. }
  209. return $headers;
  210. }
  211. /**
  212. * Adds value to the array by input name, e.g. `Item[name]`.
  213. * @param array $array array which should store value.
  214. * @param string $name input name specification.
  215. * @param mixed $value value to be added.
  216. */
  217. private function addValue(&$array, $name, $value)
  218. {
  219. $nameParts = preg_split('/\\]\\[|\\[/s', $name);
  220. $current = &$array;
  221. foreach ($nameParts as $namePart) {
  222. $namePart = trim($namePart, ']');
  223. if ($namePart === '') {
  224. $current[] = [];
  225. $lastKey = array_pop(array_keys($current));
  226. $current = &$current[$lastKey];
  227. } else {
  228. if (!isset($current[$namePart])) {
  229. $current[$namePart] = [];
  230. }
  231. $current = &$current[$namePart];
  232. }
  233. }
  234. $current = $value;
  235. }
  236. /**
  237. * Adds file info to the uploaded files array by input name, e.g. `Item[file]`.
  238. * @param array $files array containing uploaded files
  239. * @param string $name input name specification.
  240. * @param array $info file info.
  241. */
  242. private function addFile(&$files, $name, $info)
  243. {
  244. if (strpos($name, '[') === false) {
  245. $files[$name] = $info;
  246. return;
  247. }
  248. $fileInfoAttributes = [
  249. 'name',
  250. 'type',
  251. 'size',
  252. 'error',
  253. 'tmp_name',
  254. 'tmp_resource'
  255. ];
  256. $nameParts = preg_split('/\\]\\[|\\[/s', $name);
  257. $baseName = array_shift($nameParts);
  258. if (!isset($files[$baseName])) {
  259. $files[$baseName] = [];
  260. foreach ($fileInfoAttributes as $attribute) {
  261. $files[$baseName][$attribute] = [];
  262. }
  263. } else {
  264. foreach ($fileInfoAttributes as $attribute) {
  265. $files[$baseName][$attribute] = (array)$files[$baseName][$attribute];
  266. }
  267. }
  268. foreach ($fileInfoAttributes as $attribute) {
  269. if (!isset($info[$attribute])) {
  270. continue;
  271. }
  272. $current = &$files[$baseName][$attribute];
  273. foreach ($nameParts as $namePart) {
  274. $namePart = trim($namePart, ']');
  275. if ($namePart === '') {
  276. $current[] = [];
  277. $lastKey = array_pop(array_keys($current));
  278. $current = &$current[$lastKey];
  279. } else {
  280. if (!isset($current[$namePart])) {
  281. $current[$namePart] = [];
  282. }
  283. $current = &$current[$namePart];
  284. }
  285. }
  286. $current = $info[$attribute];
  287. }
  288. }
  289. /**
  290. * Gets the size in bytes from verbose size representation.
  291. * For example: '5K' => 5*1024
  292. * @param string $verboseSize verbose size representation.
  293. * @return integer actual size in bytes.
  294. */
  295. private function getByteSize($verboseSize)
  296. {
  297. if (empty($verboseSize)) {
  298. return 0;
  299. }
  300. if (is_numeric($verboseSize)) {
  301. return (int) $verboseSize;
  302. }
  303. $sizeUnit = trim($verboseSize, '0123456789');
  304. $size = str_replace($sizeUnit, '', $verboseSize);
  305. $size = trim($size);
  306. if (!is_numeric($size)) {
  307. return 0;
  308. }
  309. switch (strtolower($sizeUnit)) {
  310. case 'kb':
  311. case 'k':
  312. return $size * 1024;
  313. case 'mb':
  314. case 'm':
  315. return $size * 1024 * 1024;
  316. case 'gb':
  317. case 'g':
  318. return $size * 1024 * 1024 * 1024;
  319. default:
  320. return 0;
  321. }
  322. }
  323. }