PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
woocommerce-pos / vendor_prefixed / guzzlehttp / psr7 / src / MultipartStream.php

MultipartStream.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.20, at vendor_prefixed/guzzlehttp/psr7/src/MultipartStream.php

223 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WCPOS\Vendor\GuzzleHttp\Psr7;
5
6 use WCPOS\Vendor\Psr\Http\Message\StreamInterface;
7 /**
8 * Stream that when read returns bytes for a streaming multipart or
9 * multipart/form-data stream.
10 */
11 final class MultipartStream implements StreamInterface
12 {
13 use StreamDecoratorTrait;
14 use NonSerializableStreamTrait;
15 private string $boundary;
16 private StreamInterface $stream;
17 private const BOUNDARY_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=? ";
18 /**
19 * @param array $elements Array of associative arrays, each containing a
20 * required "name" key mapping to the form field,
21 * name, a required "contents" key mapping to any
22 * non-array value accepted by Utils::streamFor()
23 * (non-string scalar field values are cast to
24 * string), or an array for nested expansion.
25 * Optional keys include "headers" (associative
26 * array of custom headers) and "filename" (string
27 * to send as the filename in the part).
28 * When "contents" is an array, it is recursively
29 * expanded into multiple fields using bracket notation
30 * (e.g., name[0][key]). Empty arrays produce no fields.
31 * The "filename" and "headers" options cannot be used
32 * with array contents.
33 * @param string|null $boundary You can optionally provide a specific boundary
34 *
35 * @throws \InvalidArgumentException
36 */
37 public function __construct(array $elements = [], ?string $boundary = null)
38 {
39 if ($boundary !== null) {
40 self::validateBoundary($boundary);
41 }
42 $this->boundary = $boundary ?? \bin2hex(\random_bytes(20));
43 $this->stream = $this->createStream($elements);
44 }
45 public function getBoundary() : string
46 {
47 return $this->boundary;
48 }
49 public function isWritable() : bool
50 {
51 return \false;
52 }
53 /**
54 * Get the headers needed before transferring the content of a POST file
55 *
56 * @param array<array-key, string> $headers
57 */
58 private function getHeaders(array $headers) : string
59 {
60 $str = '';
61 foreach ($headers as $key => $value) {
62 $key = (string) $key;
63 self::validatePartHeaderName($key);
64 self::validatePartHeaderValue($key, $value);
65 $str .= "{$key}: {$value}\r\n";
66 }
67 return "--{$this->boundary}\r\n" . \rtrim($str, "\r\n") . "\r\n\r\n";
68 }
69 /**
70 * Create the aggregate stream that will be used to upload the POST data
71 */
72 protected function createStream(array $elements = []) : StreamInterface
73 {
74 $stream = new AppendStream();
75 foreach ($elements as $element) {
76 if (!\is_array($element)) {
77 throw new \UnexpectedValueException('An array is expected');
78 }
79 $this->addElement($stream, $element);
80 }
81 // Add the trailing boundary with CRLF
82 $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n"));
83 return $stream;
84 }
85 private function addElement(AppendStream $stream, array $element) : void
86 {
87 foreach (['contents', 'name'] as $key) {
88 if (!\array_key_exists($key, $element)) {
89 throw new \InvalidArgumentException("A '{$key}' key is required");
90 }
91 }
92 if (!\is_string($element['name']) && !\is_int($element['name'])) {
93 throw new \InvalidArgumentException("The 'name' key must be a string or integer");
94 }
95 if (\is_array($element['contents'])) {
96 if (\array_key_exists('filename', $element) || \array_key_exists('headers', $element)) {
97 throw new \InvalidArgumentException("The 'filename' and 'headers' options cannot be used when 'contents' is an array");
98 }
99 $this->addNestedElements($stream, $element['contents'], (string) $element['name']);
100 return;
101 }
102 $contents = $element['contents'];
103 if (\is_scalar($contents) && !\is_string($contents)) {
104 // Multipart field values are byte strings on the wire, so finite
105 // numeric and boolean field values are cast to string here rather
106 // than rejected by streamFor(). Non-finite floats cannot be
107 // represented and are rejected.
108 if (\is_float($contents) && !\is_finite($contents)) {
109 throw new \InvalidArgumentException('Cannot create a stream from a non-finite float.');
110 }
111 $contents = (string) $contents;
112 }
113 $element['contents'] = Utils::streamFor($contents);
114 if (empty($element['filename'])) {
115 $uri = $element['contents']->getMetadata('uri');
116 if ($uri && \is_string($uri) && !\str_starts_with($uri, 'php://') && !\str_starts_with($uri, 'data://')) {
117 $element['filename'] = $uri;
118 }
119 }
120 [$body, $headers] = $this->createElement((string) $element['name'], $element['contents'], $element['filename'] ?? null, $element['headers'] ?? []);
121 $stream->addStream(Utils::streamFor($this->getHeaders($headers)));
122 $stream->addStream($body);
123 $stream->addStream(Utils::streamFor("\r\n"));
124 }
125 /**
126 * Recursively expand array contents into multiple form fields.
127 *
128 * @param array<array-key, mixed> $contents
129 */
130 private function addNestedElements(AppendStream $stream, array $contents, string $root) : void
131 {
132 foreach ($contents as $key => $value) {
133 $fieldName = $root === '' ? \sprintf('[%s]', (string) $key) : \sprintf('%s[%s]', $root, (string) $key);
134 if (\is_array($value)) {
135 $this->addNestedElements($stream, $value, $fieldName);
136 } else {
137 $this->addElement($stream, ['name' => $fieldName, 'contents' => $value]);
138 }
139 }
140 }
141 /**
142 * @param array<array-key, mixed> $headers
143 *
144 * @return array{0: StreamInterface, 1: array<array-key, string>}
145 */
146 private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers) : array
147 {
148 $headers = self::normalizePartHeaders($headers);
149 // Set a default content-disposition header if one was no provided
150 $disposition = self::getHeader($headers, 'content-disposition');
151 if (!$disposition) {
152 $escapedName = self::escapeContentDispositionParameter($name);
153 $headers['Content-Disposition'] = $filename === '0' || $filename ? \sprintf('form-data; name="%s"; filename="%s"', $escapedName, self::escapeContentDispositionParameter(\basename($filename))) : \sprintf('form-data; name="%s"', $escapedName);
154 }
155 // Set a default Content-Type if one was not supplied
156 $type = self::getHeader($headers, 'content-type');
157 if (!$type && ($filename === '0' || $filename)) {
158 $headers['Content-Type'] = MimeType::fromFilename($filename) ?? 'application/octet-stream';
159 }
160 return [$stream, $headers];
161 }
162 /**
163 * @param array<array-key, string> $headers
164 */
165 private static function getHeader(array $headers, string $key) : ?string
166 {
167 $lowercaseHeader = Utils::asciiToLower($key);
168 foreach ($headers as $k => $v) {
169 if (Utils::asciiToLower((string) $k) === $lowercaseHeader) {
170 return $v;
171 }
172 }
173 return null;
174 }
175 private static function validateBoundary(string $boundary) : void
176 {
177 $length = \strlen($boundary);
178 if ($length < 1 || $length > 70 || $boundary[$length - 1] === ' ') {
179 throw new \InvalidArgumentException('Invalid multipart boundary.');
180 }
181 if (\strspn($boundary, self::BOUNDARY_CHARS) !== $length) {
182 throw new \InvalidArgumentException('Invalid multipart boundary.');
183 }
184 }
185 /**
186 * @param array<array-key, mixed> $headers
187 *
188 * @return array<array-key, string>
189 */
190 private static function normalizePartHeaders(array $headers) : array
191 {
192 $normalized = [];
193 foreach ($headers as $key => $value) {
194 $key = (string) $key;
195 self::validatePartHeaderName($key);
196 if (!\is_string($value)) {
197 throw new \InvalidArgumentException('Multipart part header value must be a string.');
198 }
199 self::validatePartHeaderValue($key, $value);
200 $normalized[$key] = $value;
201 }
202 return $normalized;
203 }
204 private static function validatePartHeaderName(string $name) : void
205 {
206 if (!Rfc9110::isToken($name)) {
207 throw new \InvalidArgumentException(\sprintf('Invalid multipart part header name: %s', DiagnosticValue::escape($name)));
208 }
209 }
210 private static function validatePartHeaderValue(string $name, string $value) : void
211 {
212 if (!Rfc9110::isFieldValue($value)) {
213 $reason = \strpbrk($value, "\r\n") !== \false ? 'must not contain CR or LF characters' : 'contains an invalid control character';
214 throw new \InvalidArgumentException(\sprintf('Multipart part header "%s" %s.', DiagnosticValue::escape($name), $reason));
215 }
216 }
217 private static function escapeContentDispositionParameter(string $value) : string
218 {
219 // Match WHATWG browser multipart/form-data behavior: escape CR, LF, and DQUOTE only.
220 return \str_replace(["\r", "\n", '"'], ['%0D', '%0A', '%22'], $value);
221 }
222 }
223