Completed
Push — master ( 9addfc...8819e7 )
by Tobias
01:16
created

MultipartStreamBuilder::getBoundary()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
namespace Http\Message\MultipartStream;
4
5
use Http\Discovery\Exception\NotFoundException;
6
use Http\Discovery\Psr17FactoryDiscovery;
7
use Http\Discovery\StreamFactoryDiscovery;
8
use Http\Message\StreamFactory as HttplugStreamFactory;
9
use Psr\Http\Message\StreamFactoryInterface;
10
use Psr\Http\Message\StreamInterface;
11
12
/**
13
 * Build your own Multipart stream. A Multipart stream is a collection of streams separated with a $bounary. This
14
 * class helps you to create a Multipart stream with stream implementations from any PSR7 library.
15
 *
16
 * @author Michael Dowling and contributors to guzzlehttp/psr7
17
 * @author Tobias Nyholm <[email protected]>
18
 */
19
class MultipartStreamBuilder
20
{
21
    /**
22
     * @var HttplugStreamFactory|StreamFactoryInterface
23
     */
24
    private $streamFactory;
25
26
    /**
27
     * @var MimetypeHelper
28
     */
29
    private $mimetypeHelper;
30
31
    /**
32
     * @var string
33
     */
34
    private $boundary;
35
36
    /**
37
     * @var array Element where each Element is an array with keys ['contents', 'headers']
38
     */
39
    private $data = [];
40
41
    /**
42
     * @param HttplugStreamFactory|StreamFactoryInterface|null $streamFactory
43
     */
44 15
    public function __construct($streamFactory = null)
45
    {
46 15
        if ($streamFactory instanceof StreamFactoryInterface || $streamFactory instanceof HttplugStreamFactory) {
47 2
            $this->streamFactory = $streamFactory;
48
49 2
            return;
50
        }
51
52 13
        if (null !== $streamFactory) {
53 1
            throw new \LogicException(sprintf(
54 1
                'First arguemnt to the constructor of "%s" must be of type "%s", "%s" or null. Got %s',
55 1
                __CLASS__,
56 1
                StreamFactoryInterface::class,
57 1
                HttplugStreamFactory::class,
58 1
                \is_object($streamFactory) ? \get_class($streamFactory) : \gettype($streamFactory)
59
            ));
60
        }
61
62
        // Try to find a stream factory.
63
        try {
64 12
            $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory();
65
        } catch (NotFoundException $psr17Exception) {
66
            try {
67
                $this->streamFactory = StreamFactoryDiscovery::find();
68
            } catch (NotFoundException $httplugException) {
69
                // we could not find any factory.
70
                throw $psr17Exception;
71
            }
72
        }
73 12
    }
74
75
    /**
76
     * Add a resource to the Multipart Stream.
77
     *
78
     * @param string|resource|\Psr\Http\Message\StreamInterface $resource the filepath, resource or StreamInterface of the data
79
     * @param array                                             $headers  additional headers array: ['header-name' => 'header-value']
80
     *
81
     * @return MultipartStreamBuilder
82
     */
83 13
    public function addData($resource, array $headers = [])
84
    {
85 13
        $stream = $this->createStream($resource);
86 13
        $this->data[] = ['contents' => $stream, 'headers' => $headers];
87
88 13
        return $this;
89
    }
90
91
    /**
92
     * Add a resource to the Multipart Stream.
93
     *
94
     * @param string                          $name     the formpost name
95
     * @param string|resource|StreamInterface $resource
96
     * @param array                           $options  {
97
     *
98
     *     @var array $headers additional headers ['header-name' => 'header-value']
99
     *     @var string $filename
100
     * }
101
     *
102
     * @return MultipartStreamBuilder
103
     */
104 14
    public function addResource($name, $resource, array $options = [])
105
    {
106 14
        $stream = $this->createStream($resource);
107
108
        // validate options['headers'] exists
109 13
        if (!isset($options['headers'])) {
110 12
            $options['headers'] = [];
111
        }
112
113
        // Try to add filename if it is missing
114 13
        if (empty($options['filename'])) {
115 12
            $options['filename'] = null;
116 12
            $uri = $stream->getMetadata('uri');
117 12
            if ('php://' !== substr($uri, 0, 6)) {
118 5
                $options['filename'] = $uri;
119
            }
120
        }
121
122 13
        $this->prepareHeaders($name, $stream, $options['filename'], $options['headers']);
123
124 13
        return $this->addData($stream, $options['headers']);
125
    }
126
127
    /**
128
     * Build the stream.
129
     *
130
     * @return StreamInterface
131
     */
132 13
    public function build()
133
    {
134
        // Open a temporary read-write stream as buffer.
135
        // If the size is less than predefined limit, things will stay in memory.
136
        // If the size is more than that, things will be stored in temp file.
137 13
        $buffer = fopen('php://temp', 'r+');
138 13
        foreach ($this->data as $data) {
139
            // Add start and headers
140 12
            fwrite($buffer, "--{$this->getBoundary()}\r\n".
141 12
                $this->getHeaders($data['headers'])."\r\n");
142
143
            /** @var $contentStream StreamInterface */
144 12
            $contentStream = $data['contents'];
145
146
            // Read stream into buffer
147 12
            if ($contentStream->isSeekable()) {
148 11
                $contentStream->rewind(); // rewind to beginning.
149
            }
150 12
            if ($contentStream->isReadable()) {
151 12
                while (!$contentStream->eof()) {
152
                    // Read 1MB chunk into buffer until reached EOF.
153 12
                    fwrite($buffer, $contentStream->read(1048576));
154
                }
155
            } else {
156
                fwrite($buffer, $contentStream->__toString());
157
            }
158 12
            fwrite($buffer, "\r\n");
159
        }
160
161
        // Append end
162 13
        fwrite($buffer, "--{$this->getBoundary()}--\r\n");
163
164
        // Rewind to starting position for reading.
165 13
        fseek($buffer, 0);
166
167 13
        return $this->createStream($buffer);
168
    }
169
170
    /**
171
     * Add extra headers if they are missing.
172
     *
173
     * @param string $name
174
     * @param string $filename
175
     * @param array  &$headers
176
     */
177 13
    private function prepareHeaders($name, StreamInterface $stream, $filename, array &$headers)
178
    {
179 13
        $hasFilename = '0' === $filename || $filename;
180
181
        // Set a default content-disposition header if one was not provided
182 13
        if (!$this->hasHeader($headers, 'content-disposition')) {
183 12
            $headers['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
184 12
            if ($hasFilename) {
185 6
                $headers['Content-Disposition'] .= sprintf('; filename="%s"', $this->basename($filename));
186
            }
187
        }
188
189
        // Set a default content-length header if one was not provided
190 13
        if (!$this->hasHeader($headers, 'content-length')) {
191 12
            if ($length = $stream->getSize()) {
192 11
                $headers['Content-Length'] = (string) $length;
193
            }
194
        }
195
196
        // Set a default Content-Type if one was not provided
197 13
        if (!$this->hasHeader($headers, 'content-type') && $hasFilename) {
198 6
            if ($type = $this->getMimetypeHelper()->getMimetypeFromFilename($filename)) {
199 6
                $headers['Content-Type'] = $type;
200
            }
201
        }
202 13
    }
203
204
    /**
205
     * Get the headers formatted for the HTTP message.
206
     *
207
     * @return string
208
     */
209 12
    private function getHeaders(array $headers)
210
    {
211 12
        $str = '';
212 12
        foreach ($headers as $key => $value) {
213 12
            $str .= sprintf("%s: %s\r\n", $key, $value);
214
        }
215
216 12
        return $str;
217
    }
218
219
    /**
220
     * Check if header exist.
221
     *
222
     * @param string $key case insensitive
223
     *
224
     * @return bool
225
     */
226 13
    private function hasHeader(array $headers, $key)
227
    {
228 13
        $lowercaseHeader = strtolower($key);
229 13
        foreach ($headers as $k => $v) {
230 13
            if (strtolower($k) === $lowercaseHeader) {
231 1
                return true;
232
            }
233
        }
234
235 12
        return false;
236
    }
237
238
    /**
239
     * Get the boundary that separates the streams.
240
     *
241
     * @return string
242
     */
243 13
    public function getBoundary()
244
    {
245 13
        if (null === $this->boundary) {
246 12
            $this->boundary = uniqid('', true);
247
        }
248
249 13
        return $this->boundary;
250
    }
251
252
    /**
253
     * @param string $boundary
254
     *
255
     * @return MultipartStreamBuilder
256
     */
257 2
    public function setBoundary($boundary)
258
    {
259 2
        $this->boundary = $boundary;
260
261 2
        return $this;
262
    }
263
264
    /**
265
     * @return MimetypeHelper
266
     */
267 6
    private function getMimetypeHelper()
268
    {
269 6
        if (null === $this->mimetypeHelper) {
270 6
            $this->mimetypeHelper = new ApacheMimetypeHelper();
271
        }
272
273 6
        return $this->mimetypeHelper;
274
    }
275
276
    /**
277
     * If you have custom file extension you may overwrite the default MimetypeHelper with your own.
278
     *
279
     * @return MultipartStreamBuilder
280
     */
281
    public function setMimetypeHelper(MimetypeHelper $mimetypeHelper)
282
    {
283
        $this->mimetypeHelper = $mimetypeHelper;
284
285
        return $this;
286
    }
287
288
    /**
289
     * Reset and clear all stored data. This allows you to use builder for a subsequent request.
290
     *
291
     * @return MultipartStreamBuilder
292
     */
293 1
    public function reset()
294
    {
295 1
        $this->data = [];
296 1
        $this->boundary = null;
297
298 1
        return $this;
299
    }
300
301
    /**
302
     * Gets the filename from a given path.
303
     *
304
     * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character.
305
     *
306
     * @author Drupal 8.2
307
     *
308
     * @param string $path
309
     *
310
     * @return string
311
     */
312 6
    private function basename($path)
313
    {
314 6
        $separators = '/';
315 6
        if (DIRECTORY_SEPARATOR != '/') {
316
            // For Windows OS add special separator.
317
            $separators .= DIRECTORY_SEPARATOR;
318
        }
319
320
        // Remove right-most slashes when $path points to directory.
321 6
        $path = rtrim($path, $separators);
322
323
        // Returns the trailing part of the $path starting after one of the directory separators.
324 6
        $filename = preg_match('@[^'.preg_quote($separators, '@').']+$@', $path, $matches) ? $matches[0] : '';
325
326 6
        return $filename;
327
    }
328
329
    /**
330
     * @param string|resource|StreamInterface $resource
331
     *
332
     * @return StreamInterface
333
     */
334 14
    private function createStream($resource)
335
    {
336 14
        if ($resource instanceof StreamInterface) {
337 13
            return $resource;
338
        }
339
340 14
        if ($this->streamFactory instanceof HttplugStreamFactory) {
341 1
            return $this->streamFactory->createStream($resource);
342
        }
343
344
        // Assert: We are using a PSR17 stream factory.
345 13
        if (\is_string($resource)) {
346 6
            return $this->streamFactory->createStream($resource);
347
        }
348
349 13
        if (\is_resource($resource)) {
350 12
            return $this->streamFactory->createStreamFromResource($resource);
351
        }
352
353 1
        throw new \InvalidArgumentException(sprintf('First argument to "%s::createStream()" must be a string, resource or StreamInterface.', __CLASS__));
354
    }
355
}
356