MultipartFormDataParser.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <?php
  2. namespace Libs;
  3. use Psr\Http\Message\ServerRequestInterface;
  4. class MultipartFormDataParser
  5. {
  6. public static function parse(ServerRequestInterface $request): array
  7. {
  8. $contentType = $request->getHeaderLine('Content-Type');
  9. $rawBody = '';
  10. $stream = $request->getBody();
  11. $streamMeta = [];
  12. if (is_object($stream)) {
  13. if (method_exists($stream, 'isSeekable')) {
  14. $streamMeta['seekable'] = (bool)$stream->isSeekable();
  15. }
  16. if (method_exists($stream, 'isReadable')) {
  17. $streamMeta['readable'] = (bool)$stream->isReadable();
  18. }
  19. if (method_exists($stream, 'getSize')) {
  20. $streamMeta['size'] = $stream->getSize();
  21. }
  22. if (method_exists($stream, 'tell')) {
  23. try {
  24. $streamMeta['tell'] = $stream->tell();
  25. } catch (\Throwable $e) {
  26. }
  27. }
  28. }
  29. if (is_object($stream) && method_exists($stream, 'getContents')) {
  30. if (method_exists($stream, 'rewind')) {
  31. try {
  32. $stream->rewind();
  33. } catch (\Throwable $e) {
  34. }
  35. }
  36. $rawBody = (string)$stream->getContents();
  37. if (method_exists($stream, 'rewind')) {
  38. try {
  39. $stream->rewind();
  40. } catch (\Throwable $e) {
  41. }
  42. }
  43. } else {
  44. $rawBody = (string)$stream;
  45. }
  46. if ($rawBody === '') {
  47. $rawBody = (string)file_get_contents('php://input');
  48. }
  49. return self::parseBody($contentType, $rawBody, $streamMeta);
  50. }
  51. public static function parseBody(string $contentType, string $rawBody, array $streamMeta = []): array
  52. {
  53. if (!preg_match('/multipart\/form-data;\s*boundary=(?:(?:"([^"]+)")|([^;\s]+))/i', $contentType, $m)) {
  54. throw new \RuntimeException('Invalid multipart Content-Type');
  55. }
  56. $boundary = (isset($m[1]) && $m[1] !== '') ? $m[1] : ($m[2] ?? '');
  57. if ($boundary === '') {
  58. throw new \RuntimeException('Missing multipart boundary');
  59. }
  60. $delimiter = "--" . $boundary;
  61. $parts = explode($delimiter, $rawBody);
  62. $fields = [];
  63. $files = [];
  64. $meta = [
  65. 'content_type' => $contentType,
  66. 'boundary' => $boundary,
  67. 'stream' => $streamMeta,
  68. 'raw_length' => strlen($rawBody),
  69. 'parts_count' => count($parts),
  70. 'detected_parts' => [],
  71. ];
  72. foreach ($parts as $part) {
  73. $part = ltrim($part, "\r\n\n");
  74. $part = rtrim($part, "\r\n\n");
  75. if ($part === '' || $part === '--') {
  76. continue;
  77. }
  78. $split = preg_split("/\r?\n\r?\n/", $part, 2);
  79. if (!$split || count($split) !== 2) {
  80. continue;
  81. }
  82. [$rawHeaders, $body] = $split;
  83. $headers = self::parseHeaders($rawHeaders);
  84. $cd = $headers['content-disposition'] ?? '';
  85. if (!preg_match('/name="([^"]+)"/i', $cd, $nm)) {
  86. continue;
  87. }
  88. $name = $nm[1];
  89. $filename = null;
  90. if (preg_match('/filename="([^"]*)"/i', $cd, $fm)) {
  91. $filename = $fm[1];
  92. }
  93. $meta['detected_parts'][] = [
  94. 'name' => $name,
  95. 'has_filename' => $filename !== null && $filename !== '',
  96. 'filename' => ($filename !== null && $filename !== '') ? $filename : null,
  97. 'content_type' => $headers['content-type'] ?? null,
  98. ];
  99. $body = preg_replace("/\r\n\z/", '', $body);
  100. if ($filename !== null && $filename !== '') {
  101. $files[$name] = [
  102. 'field' => $name,
  103. 'filename' => $filename,
  104. 'content_type' => $headers['content-type'] ?? 'application/octet-stream',
  105. 'content' => $body,
  106. ];
  107. } else {
  108. $fields[$name] = $body;
  109. }
  110. }
  111. return ['fields' => $fields, 'files' => $files, 'meta' => $meta];
  112. }
  113. private static function parseHeaders(string $rawHeaders): array
  114. {
  115. $headers = [];
  116. foreach (preg_split('/\r?\n/', $rawHeaders) as $line) {
  117. $line = trim($line);
  118. if ($line === '' || strpos($line, ':') === false) {
  119. continue;
  120. }
  121. [$k, $v] = explode(':', $line, 2);
  122. $headers[strtolower(trim($k))] = trim($v);
  123. }
  124. return $headers;
  125. }
  126. }