Completed
Push — master ( 968b64...1f5822 )
by Tobias
01:50
created

MultipartStreamBuilder::basename()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 4
nop 1
1
<?php
2
3
namespace Http\Message\MultipartStream;
4
5
use Http\Discovery\StreamFactoryDiscovery;
6
use Http\Message\StreamFactory;
7
use Psr\Http\Message\StreamInterface;
8
9
/**
10
 * Build your own Multipart stream. A Multipart stream is a collection of streams separated with a $bounary. This
11
 * class helps you to create a Multipart stream with stream implementations from any PSR7 library.
12
 *
13
 * @author Michael Dowling and contributors to guzzlehttp/psr7
14
 * @author Tobias Nyholm <[email protected]>
15
 */
16
class MultipartStreamBuilder
17
{
18
    /**
19
     * @var StreamFactory
20
     */
21
    private $streamFactory;
22
23
    /**
24
     * @var MimetypeHelper
25
     */
26
    private $mimetypeHelper;
27
28
    /**
29
     * @var string
30
     */
31
    private $boundary;
32
33
    /**
34
     * @var array Element where each Element is an array with keys ['contents', 'headers', 'filename']
35
     */
36
    private $data = [];
37
38
    /**
39
     * @param StreamFactory|null $streamFactory
40
     */
41
    public function __construct(StreamFactory $streamFactory = null)
42
    {
43
        $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
44
    }
45
46
    /**
47
     * Add a resource to the Multipart Stream. If the same $name is used twice the first resource will
48
     * be overwritten.
49
     *
50
     * @param string                          $name     the formpost name
51
     * @param string|resource|StreamInterface $resource
52
     * @param array                           $options  {
53
     *
54
     *     @var array $headers additional headers ['header-name' => 'header-value']
55
     *     @var string $filename
56
     * }
57
     *
58
     * @return MultipartStreamBuilder
59
     */
60
    public function addResource($name, $resource, array $options = [])
61
    {
62
        $stream = $this->streamFactory->createStream($resource);
63
64
        // validate options['headers'] exists
65
        if (!isset($options['headers'])) {
66
            $options['headers'] = [];
67
        }
68
69
        // Try to add filename if it is missing
70
        if (empty($options['filename'])) {
71
            $options['filename'] = null;
72
            $uri = $stream->getMetadata('uri');
73
            if (substr($uri, 0, 6) !== 'php://') {
74
                $options['filename'] = $uri;
75
            }
76
        }
77
78
        $this->prepareHeaders($name, $stream, $options['filename'], $options['headers']);
79
        $this->data[$name] = ['contents' => $stream, 'headers' => $options['headers'], 'filename' => $options['filename']];
80
81
        return $this;
82
    }
83
84
    /**
85
     * Build the stream.
86
     *
87
     * @return StreamInterface
88
     */
89
    public function build()
90
    {
91
        $streams = '';
92
        foreach ($this->data as $data) {
93
            // Add start and headers
94
            $streams .= "--{$this->getBoundary()}\r\n".
95
                $this->getHeaders($data['headers'])."\r\n";
96
97
            // Convert the stream to string
98
            $streams .= (string) $data['contents'];
99
            $streams .= "\r\n";
100
        }
101
102
        // Append end
103
        $streams .= "--{$this->getBoundary()}--\r\n";
104
105
        return $this->streamFactory->createStream($streams);
106
    }
107
108
    /**
109
     * Add extra headers if they are missing.
110
     *
111
     * @param string          $name
112
     * @param StreamInterface $stream
113
     * @param string          $filename
114
     * @param array           &$headers
115
     */
116
    private function prepareHeaders($name, StreamInterface $stream, $filename, array &$headers)
117
    {
118
        $hasFilename = $filename === '0' || $filename;
119
120
        // Set a default content-disposition header if one was not provided
121
        if (!$this->hasHeader($headers, 'content-disposition')) {
122
            $headers['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
123
            if ($hasFilename) {
124
                $headers['Content-Disposition'] .= sprintf('; filename="%s"', $this->basename($filename));
125
            }
126
        }
127
128
        // Set a default content-length header if one was not provided
129
        if (!$this->hasHeader($headers, 'content-length')) {
130
            if ($length = $stream->getSize()) {
131
                $headers['Content-Length'] = (string) $length;
132
            }
133
        }
134
135
        // Set a default Content-Type if one was not provided
136
        if (!$this->hasHeader($headers, 'content-type') && $hasFilename) {
137
            if ($type = $this->getMimetypeHelper()->getMimetypeFromFilename($filename)) {
138
                $headers['Content-Type'] = $type;
139
            }
140
        }
141
    }
142
143
    /**
144
     * Get the headers formatted for the HTTP message.
145
     *
146
     * @param array $headers
147
     *
148
     * @return string
149
     */
150
    private function getHeaders(array $headers)
151
    {
152
        $str = '';
153
        foreach ($headers as $key => $value) {
154
            $str .= sprintf("%s: %s\r\n", $key, $value);
155
        }
156
157
        return $str;
158
    }
159
160
    /**
161
     * Check if header exist.
162
     *
163
     * @param array  $headers
164
     * @param string $key     case insensitive
165
     *
166
     * @return bool
167
     */
168
    private function hasHeader(array $headers, $key)
169
    {
170
        $lowercaseHeader = strtolower($key);
171
        foreach ($headers as $k => $v) {
172
            if (strtolower($k) === $lowercaseHeader) {
173
                return true;
174
            }
175
        }
176
177
        return false;
178
    }
179
180
    /**
181
     * Get the boundary that separates the streams.
182
     *
183
     * @return string
184
     */
185
    public function getBoundary()
186
    {
187
        if ($this->boundary === null) {
188
            $this->boundary = uniqid();
189
        }
190
191
        return $this->boundary;
192
    }
193
194
    /**
195
     * @param string $boundary
196
     *
197
     * @return MultipartStreamBuilder
198
     */
199
    public function setBoundary($boundary)
200
    {
201
        $this->boundary = $boundary;
202
203
        return $this;
204
    }
205
206
    /**
207
     * @return MimetypeHelper
208
     */
209
    private function getMimetypeHelper()
210
    {
211
        if ($this->mimetypeHelper === null) {
212
            $this->mimetypeHelper = new ApacheMimetypeHelper();
213
        }
214
215
        return $this->mimetypeHelper;
216
    }
217
218
    /**
219
     * If you have custom file extension you may overwrite the default MimetypeHelper with your own.
220
     *
221
     * @param MimetypeHelper $mimetypeHelper
222
     *
223
     * @return MultipartStreamBuilder
224
     */
225
    public function setMimetypeHelper(MimetypeHelper $mimetypeHelper)
226
    {
227
        $this->mimetypeHelper = $mimetypeHelper;
228
229
        return $this;
230
    }
231
232
    /**
233
     * Gets the filename from a given path.
234
     *
235
     * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character.
236
     *
237
     * @author Drupal 8.2
238
     *
239
     * @param string $path
240
     *
241
     * @return string
242
     */
243
    private function basename($path)
244
    {
245
        $separators = '/';
246
        if (DIRECTORY_SEPARATOR != '/') {
247
            // For Windows OS add special separator.
248
            $separators .= DIRECTORY_SEPARATOR;
249
        }
250
        // Remove right-most slashes when $uri points to directory.
251
        $path = rtrim($path, $separators);
252
        // Returns the trailing part of the $uri starting after one of the directory
253
        // separators.
254
        $filename = preg_match('@[^'.preg_quote($separators, '@').']+$@', $path, $matches) ? $matches[0] : '';
255
256
        return $filename;
257
    }
258
}
259