MultipartStream.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. <?php
  2. namespace GuzzleHttp\Psr7;
  3. use Psr\Http\Message\StreamInterface;
  4. /**
  5. * Stream that when read returns bytes for a streaming multipart or
  6. * multipart/form-data stream.
  7. */
  8. class MultipartStream implements StreamInterface
  9. {
  10. use StreamDecoratorTrait;
  11. private $boundary;
  12. /**
  13. * @param array $elements Array of associative arrays, each containing a
  14. * required "name" key mapping to the form field,
  15. * name, a required "contents" key mapping to a
  16. * StreamInterface/resource/string, an optional
  17. * "headers" associative array of custom headers,
  18. * and an optional "filename" key mapping to a
  19. * string to send as the filename in the part.
  20. * @param string $boundary You can optionally provide a specific boundary
  21. *
  22. * @throws \InvalidArgumentException
  23. */
  24. public function __construct(array $elements = [], $boundary = null)
  25. {
  26. $this->boundary = $boundary ?: sha1(uniqid('', true));
  27. $this->stream = $this->createStream($elements);
  28. }
  29. /**
  30. * Get the boundary
  31. *
  32. * @return string
  33. */
  34. public function getBoundary()
  35. {
  36. return $this->boundary;
  37. }
  38. public function isWritable()
  39. {
  40. return false;
  41. }
  42. /**
  43. * Get the headers needed before transferring the content of a POST file
  44. */
  45. private function getHeaders(array $headers)
  46. {
  47. $str = '';
  48. foreach ($headers as $key => $value) {
  49. $str .= "{$key}: {$value}\r\n";
  50. }
  51. return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n";
  52. }
  53. /**
  54. * Create the aggregate stream that will be used to upload the POST data
  55. */
  56. protected function createStream(array $elements)
  57. {
  58. $stream = new AppendStream();
  59. foreach ($elements as $element) {
  60. $this->addElement($stream, $element);
  61. }
  62. // Add the trailing boundary with CRLF
  63. $stream->addStream(stream_for("--{$this->boundary}--\r\n"));
  64. return $stream;
  65. }
  66. private function addElement(AppendStream $stream, array $element)
  67. {
  68. foreach (['contents', 'name'] as $key) {
  69. if (!array_key_exists($key, $element)) {
  70. throw new \InvalidArgumentException("A '{$key}' key is required");
  71. }
  72. }
  73. $element['contents'] = stream_for($element['contents']);
  74. if (empty($element['filename'])) {
  75. $uri = $element['contents']->getMetadata('uri');
  76. if (substr($uri, 0, 6) !== 'php://') {
  77. $element['filename'] = $uri;
  78. }
  79. }
  80. list($body, $headers) = $this->createElement(
  81. $element['name'],
  82. $element['contents'],
  83. isset($element['filename']) ? $element['filename'] : null,
  84. isset($element['headers']) ? $element['headers'] : []
  85. );
  86. $stream->addStream(stream_for($this->getHeaders($headers)));
  87. $stream->addStream($body);
  88. $stream->addStream(stream_for("\r\n"));
  89. }
  90. /**
  91. * @return array
  92. */
  93. private function createElement($name, StreamInterface $stream, $filename, array $headers)
  94. {
  95. // Set a default content-disposition header if one was no provided
  96. $disposition = $this->getHeader($headers, 'content-disposition');
  97. if (!$disposition) {
  98. $headers['Content-Disposition'] = ($filename === '0' || $filename)
  99. ? sprintf('form-data; name="%s"; filename="%s"',
  100. $name,
  101. basename($filename))
  102. : "form-data; name=\"{$name}\"";
  103. }
  104. // Set a default content-length header if one was no provided
  105. $length = $this->getHeader($headers, 'content-length');
  106. if (!$length) {
  107. if ($length = $stream->getSize()) {
  108. $headers['Content-Length'] = (string) $length;
  109. }
  110. }
  111. // Set a default Content-Type if one was not supplied
  112. $type = $this->getHeader($headers, 'content-type');
  113. if (!$type && ($filename === '0' || $filename)) {
  114. if ($type = mimetype_from_filename($filename)) {
  115. $headers['Content-Type'] = $type;
  116. }
  117. }
  118. return [$stream, $headers];
  119. }
  120. private function getHeader(array $headers, $key)
  121. {
  122. $lowercaseHeader = strtolower($key);
  123. foreach ($headers as $k => $v) {
  124. if (strtolower($k) === $lowercaseHeader) {
  125. return $v;
  126. }
  127. }
  128. return null;
  129. }
  130. }