@@ -13,236 +13,236 @@ |
||
13 | 13 | */ |
14 | 14 | final class AppendStream implements StreamInterface |
15 | 15 | { |
16 | - /** @var StreamInterface[] Streams being decorated */ |
|
17 | - private $streams = []; |
|
18 | - |
|
19 | - /** @var bool */ |
|
20 | - private $seekable = true; |
|
21 | - |
|
22 | - /** @var int */ |
|
23 | - private $current = 0; |
|
24 | - |
|
25 | - /** @var int */ |
|
26 | - private $pos = 0; |
|
27 | - |
|
28 | - /** |
|
29 | - * @param StreamInterface[] $streams Streams to decorate. Each stream must |
|
30 | - * be readable. |
|
31 | - */ |
|
32 | - public function __construct(array $streams = []) |
|
33 | - { |
|
34 | - foreach ($streams as $stream) { |
|
35 | - $this->addStream($stream); |
|
36 | - } |
|
37 | - } |
|
38 | - |
|
39 | - public function __toString(): string |
|
40 | - { |
|
41 | - try { |
|
42 | - $this->rewind(); |
|
43 | - |
|
44 | - return $this->getContents(); |
|
45 | - } catch (\Throwable $e) { |
|
46 | - if (\PHP_VERSION_ID >= 70400) { |
|
47 | - throw $e; |
|
48 | - } |
|
49 | - trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
50 | - |
|
51 | - return ''; |
|
52 | - } |
|
53 | - } |
|
54 | - |
|
55 | - /** |
|
56 | - * Add a stream to the AppendStream |
|
57 | - * |
|
58 | - * @param StreamInterface $stream Stream to append. Must be readable. |
|
59 | - * |
|
60 | - * @throws \InvalidArgumentException if the stream is not readable |
|
61 | - */ |
|
62 | - public function addStream(StreamInterface $stream): void |
|
63 | - { |
|
64 | - if (!$stream->isReadable()) { |
|
65 | - throw new \InvalidArgumentException('Each stream must be readable'); |
|
66 | - } |
|
67 | - |
|
68 | - // The stream is only seekable if all streams are seekable |
|
69 | - if (!$stream->isSeekable()) { |
|
70 | - $this->seekable = false; |
|
71 | - } |
|
72 | - |
|
73 | - $this->streams[] = $stream; |
|
74 | - } |
|
75 | - |
|
76 | - public function getContents(): string |
|
77 | - { |
|
78 | - return Utils::copyToString($this); |
|
79 | - } |
|
80 | - |
|
81 | - /** |
|
82 | - * Closes each attached stream. |
|
83 | - */ |
|
84 | - public function close(): void |
|
85 | - { |
|
86 | - $this->pos = $this->current = 0; |
|
87 | - $this->seekable = true; |
|
88 | - |
|
89 | - foreach ($this->streams as $stream) { |
|
90 | - $stream->close(); |
|
91 | - } |
|
92 | - |
|
93 | - $this->streams = []; |
|
94 | - } |
|
95 | - |
|
96 | - /** |
|
97 | - * Detaches each attached stream. |
|
98 | - * |
|
99 | - * Returns null as it's not clear which underlying stream resource to return. |
|
100 | - */ |
|
101 | - public function detach() |
|
102 | - { |
|
103 | - $this->pos = $this->current = 0; |
|
104 | - $this->seekable = true; |
|
105 | - |
|
106 | - foreach ($this->streams as $stream) { |
|
107 | - $stream->detach(); |
|
108 | - } |
|
109 | - |
|
110 | - $this->streams = []; |
|
111 | - |
|
112 | - return null; |
|
113 | - } |
|
114 | - |
|
115 | - public function tell(): int |
|
116 | - { |
|
117 | - return $this->pos; |
|
118 | - } |
|
119 | - |
|
120 | - /** |
|
121 | - * Tries to calculate the size by adding the size of each stream. |
|
122 | - * |
|
123 | - * If any of the streams do not return a valid number, then the size of the |
|
124 | - * append stream cannot be determined and null is returned. |
|
125 | - */ |
|
126 | - public function getSize(): ?int |
|
127 | - { |
|
128 | - $size = 0; |
|
129 | - |
|
130 | - foreach ($this->streams as $stream) { |
|
131 | - $s = $stream->getSize(); |
|
132 | - if ($s === null) { |
|
133 | - return null; |
|
134 | - } |
|
135 | - $size += $s; |
|
136 | - } |
|
137 | - |
|
138 | - return $size; |
|
139 | - } |
|
140 | - |
|
141 | - public function eof(): bool |
|
142 | - { |
|
143 | - return !$this->streams |
|
144 | - || ($this->current >= count($this->streams) - 1 |
|
145 | - && $this->streams[$this->current]->eof()); |
|
146 | - } |
|
147 | - |
|
148 | - public function rewind(): void |
|
149 | - { |
|
150 | - $this->seek(0); |
|
151 | - } |
|
152 | - |
|
153 | - /** |
|
154 | - * Attempts to seek to the given position. Only supports SEEK_SET. |
|
155 | - */ |
|
156 | - public function seek($offset, $whence = SEEK_SET): void |
|
157 | - { |
|
158 | - if (!$this->seekable) { |
|
159 | - throw new \RuntimeException('This AppendStream is not seekable'); |
|
160 | - } elseif ($whence !== SEEK_SET) { |
|
161 | - throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); |
|
162 | - } |
|
163 | - |
|
164 | - $this->pos = $this->current = 0; |
|
165 | - |
|
166 | - // Rewind each stream |
|
167 | - foreach ($this->streams as $i => $stream) { |
|
168 | - try { |
|
169 | - $stream->rewind(); |
|
170 | - } catch (\Exception $e) { |
|
171 | - throw new \RuntimeException('Unable to seek stream ' |
|
172 | - .$i.' of the AppendStream', 0, $e); |
|
173 | - } |
|
174 | - } |
|
175 | - |
|
176 | - // Seek to the actual position by reading from each stream |
|
177 | - while ($this->pos < $offset && !$this->eof()) { |
|
178 | - $result = $this->read(min(8096, $offset - $this->pos)); |
|
179 | - if ($result === '') { |
|
180 | - break; |
|
181 | - } |
|
182 | - } |
|
183 | - } |
|
184 | - |
|
185 | - /** |
|
186 | - * Reads from all of the appended streams until the length is met or EOF. |
|
187 | - */ |
|
188 | - public function read($length): string |
|
189 | - { |
|
190 | - $buffer = ''; |
|
191 | - $total = count($this->streams) - 1; |
|
192 | - $remaining = $length; |
|
193 | - $progressToNext = false; |
|
194 | - |
|
195 | - while ($remaining > 0) { |
|
196 | - // Progress to the next stream if needed. |
|
197 | - if ($progressToNext || $this->streams[$this->current]->eof()) { |
|
198 | - $progressToNext = false; |
|
199 | - if ($this->current === $total) { |
|
200 | - break; |
|
201 | - } |
|
202 | - ++$this->current; |
|
203 | - } |
|
204 | - |
|
205 | - $result = $this->streams[$this->current]->read($remaining); |
|
206 | - |
|
207 | - if ($result === '') { |
|
208 | - $progressToNext = true; |
|
209 | - continue; |
|
210 | - } |
|
211 | - |
|
212 | - $buffer .= $result; |
|
213 | - $remaining = $length - strlen($buffer); |
|
214 | - } |
|
215 | - |
|
216 | - $this->pos += strlen($buffer); |
|
217 | - |
|
218 | - return $buffer; |
|
219 | - } |
|
220 | - |
|
221 | - public function isReadable(): bool |
|
222 | - { |
|
223 | - return true; |
|
224 | - } |
|
225 | - |
|
226 | - public function isWritable(): bool |
|
227 | - { |
|
228 | - return false; |
|
229 | - } |
|
230 | - |
|
231 | - public function isSeekable(): bool |
|
232 | - { |
|
233 | - return $this->seekable; |
|
234 | - } |
|
235 | - |
|
236 | - public function write($string): int |
|
237 | - { |
|
238 | - throw new \RuntimeException('Cannot write to an AppendStream'); |
|
239 | - } |
|
240 | - |
|
241 | - /** |
|
242 | - * @return mixed |
|
243 | - */ |
|
244 | - public function getMetadata($key = null) |
|
245 | - { |
|
246 | - return $key ? null : []; |
|
247 | - } |
|
16 | + /** @var StreamInterface[] Streams being decorated */ |
|
17 | + private $streams = []; |
|
18 | + |
|
19 | + /** @var bool */ |
|
20 | + private $seekable = true; |
|
21 | + |
|
22 | + /** @var int */ |
|
23 | + private $current = 0; |
|
24 | + |
|
25 | + /** @var int */ |
|
26 | + private $pos = 0; |
|
27 | + |
|
28 | + /** |
|
29 | + * @param StreamInterface[] $streams Streams to decorate. Each stream must |
|
30 | + * be readable. |
|
31 | + */ |
|
32 | + public function __construct(array $streams = []) |
|
33 | + { |
|
34 | + foreach ($streams as $stream) { |
|
35 | + $this->addStream($stream); |
|
36 | + } |
|
37 | + } |
|
38 | + |
|
39 | + public function __toString(): string |
|
40 | + { |
|
41 | + try { |
|
42 | + $this->rewind(); |
|
43 | + |
|
44 | + return $this->getContents(); |
|
45 | + } catch (\Throwable $e) { |
|
46 | + if (\PHP_VERSION_ID >= 70400) { |
|
47 | + throw $e; |
|
48 | + } |
|
49 | + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
50 | + |
|
51 | + return ''; |
|
52 | + } |
|
53 | + } |
|
54 | + |
|
55 | + /** |
|
56 | + * Add a stream to the AppendStream |
|
57 | + * |
|
58 | + * @param StreamInterface $stream Stream to append. Must be readable. |
|
59 | + * |
|
60 | + * @throws \InvalidArgumentException if the stream is not readable |
|
61 | + */ |
|
62 | + public function addStream(StreamInterface $stream): void |
|
63 | + { |
|
64 | + if (!$stream->isReadable()) { |
|
65 | + throw new \InvalidArgumentException('Each stream must be readable'); |
|
66 | + } |
|
67 | + |
|
68 | + // The stream is only seekable if all streams are seekable |
|
69 | + if (!$stream->isSeekable()) { |
|
70 | + $this->seekable = false; |
|
71 | + } |
|
72 | + |
|
73 | + $this->streams[] = $stream; |
|
74 | + } |
|
75 | + |
|
76 | + public function getContents(): string |
|
77 | + { |
|
78 | + return Utils::copyToString($this); |
|
79 | + } |
|
80 | + |
|
81 | + /** |
|
82 | + * Closes each attached stream. |
|
83 | + */ |
|
84 | + public function close(): void |
|
85 | + { |
|
86 | + $this->pos = $this->current = 0; |
|
87 | + $this->seekable = true; |
|
88 | + |
|
89 | + foreach ($this->streams as $stream) { |
|
90 | + $stream->close(); |
|
91 | + } |
|
92 | + |
|
93 | + $this->streams = []; |
|
94 | + } |
|
95 | + |
|
96 | + /** |
|
97 | + * Detaches each attached stream. |
|
98 | + * |
|
99 | + * Returns null as it's not clear which underlying stream resource to return. |
|
100 | + */ |
|
101 | + public function detach() |
|
102 | + { |
|
103 | + $this->pos = $this->current = 0; |
|
104 | + $this->seekable = true; |
|
105 | + |
|
106 | + foreach ($this->streams as $stream) { |
|
107 | + $stream->detach(); |
|
108 | + } |
|
109 | + |
|
110 | + $this->streams = []; |
|
111 | + |
|
112 | + return null; |
|
113 | + } |
|
114 | + |
|
115 | + public function tell(): int |
|
116 | + { |
|
117 | + return $this->pos; |
|
118 | + } |
|
119 | + |
|
120 | + /** |
|
121 | + * Tries to calculate the size by adding the size of each stream. |
|
122 | + * |
|
123 | + * If any of the streams do not return a valid number, then the size of the |
|
124 | + * append stream cannot be determined and null is returned. |
|
125 | + */ |
|
126 | + public function getSize(): ?int |
|
127 | + { |
|
128 | + $size = 0; |
|
129 | + |
|
130 | + foreach ($this->streams as $stream) { |
|
131 | + $s = $stream->getSize(); |
|
132 | + if ($s === null) { |
|
133 | + return null; |
|
134 | + } |
|
135 | + $size += $s; |
|
136 | + } |
|
137 | + |
|
138 | + return $size; |
|
139 | + } |
|
140 | + |
|
141 | + public function eof(): bool |
|
142 | + { |
|
143 | + return !$this->streams |
|
144 | + || ($this->current >= count($this->streams) - 1 |
|
145 | + && $this->streams[$this->current]->eof()); |
|
146 | + } |
|
147 | + |
|
148 | + public function rewind(): void |
|
149 | + { |
|
150 | + $this->seek(0); |
|
151 | + } |
|
152 | + |
|
153 | + /** |
|
154 | + * Attempts to seek to the given position. Only supports SEEK_SET. |
|
155 | + */ |
|
156 | + public function seek($offset, $whence = SEEK_SET): void |
|
157 | + { |
|
158 | + if (!$this->seekable) { |
|
159 | + throw new \RuntimeException('This AppendStream is not seekable'); |
|
160 | + } elseif ($whence !== SEEK_SET) { |
|
161 | + throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); |
|
162 | + } |
|
163 | + |
|
164 | + $this->pos = $this->current = 0; |
|
165 | + |
|
166 | + // Rewind each stream |
|
167 | + foreach ($this->streams as $i => $stream) { |
|
168 | + try { |
|
169 | + $stream->rewind(); |
|
170 | + } catch (\Exception $e) { |
|
171 | + throw new \RuntimeException('Unable to seek stream ' |
|
172 | + .$i.' of the AppendStream', 0, $e); |
|
173 | + } |
|
174 | + } |
|
175 | + |
|
176 | + // Seek to the actual position by reading from each stream |
|
177 | + while ($this->pos < $offset && !$this->eof()) { |
|
178 | + $result = $this->read(min(8096, $offset - $this->pos)); |
|
179 | + if ($result === '') { |
|
180 | + break; |
|
181 | + } |
|
182 | + } |
|
183 | + } |
|
184 | + |
|
185 | + /** |
|
186 | + * Reads from all of the appended streams until the length is met or EOF. |
|
187 | + */ |
|
188 | + public function read($length): string |
|
189 | + { |
|
190 | + $buffer = ''; |
|
191 | + $total = count($this->streams) - 1; |
|
192 | + $remaining = $length; |
|
193 | + $progressToNext = false; |
|
194 | + |
|
195 | + while ($remaining > 0) { |
|
196 | + // Progress to the next stream if needed. |
|
197 | + if ($progressToNext || $this->streams[$this->current]->eof()) { |
|
198 | + $progressToNext = false; |
|
199 | + if ($this->current === $total) { |
|
200 | + break; |
|
201 | + } |
|
202 | + ++$this->current; |
|
203 | + } |
|
204 | + |
|
205 | + $result = $this->streams[$this->current]->read($remaining); |
|
206 | + |
|
207 | + if ($result === '') { |
|
208 | + $progressToNext = true; |
|
209 | + continue; |
|
210 | + } |
|
211 | + |
|
212 | + $buffer .= $result; |
|
213 | + $remaining = $length - strlen($buffer); |
|
214 | + } |
|
215 | + |
|
216 | + $this->pos += strlen($buffer); |
|
217 | + |
|
218 | + return $buffer; |
|
219 | + } |
|
220 | + |
|
221 | + public function isReadable(): bool |
|
222 | + { |
|
223 | + return true; |
|
224 | + } |
|
225 | + |
|
226 | + public function isWritable(): bool |
|
227 | + { |
|
228 | + return false; |
|
229 | + } |
|
230 | + |
|
231 | + public function isSeekable(): bool |
|
232 | + { |
|
233 | + return $this->seekable; |
|
234 | + } |
|
235 | + |
|
236 | + public function write($string): int |
|
237 | + { |
|
238 | + throw new \RuntimeException('Cannot write to an AppendStream'); |
|
239 | + } |
|
240 | + |
|
241 | + /** |
|
242 | + * @return mixed |
|
243 | + */ |
|
244 | + public function getMetadata($key = null) |
|
245 | + { |
|
246 | + return $key ? null : []; |
|
247 | + } |
|
248 | 248 | } |
@@ -46,7 +46,7 @@ |
||
46 | 46 | if (\PHP_VERSION_ID >= 70400) { |
47 | 47 | throw $e; |
48 | 48 | } |
49 | - trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
49 | + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string)$e), E_USER_ERROR); |
|
50 | 50 | |
51 | 51 | return ''; |
52 | 52 | } |
@@ -11,8 +11,7 @@ |
||
11 | 11 | * |
12 | 12 | * This is a read-only stream decorator. |
13 | 13 | */ |
14 | -final class AppendStream implements StreamInterface |
|
15 | -{ |
|
14 | +final class AppendStream implements StreamInterface { |
|
16 | 15 | /** @var StreamInterface[] Streams being decorated */ |
17 | 16 | private $streams = []; |
18 | 17 |
@@ -15,197 +15,197 @@ |
||
15 | 15 | */ |
16 | 16 | final class UriResolver |
17 | 17 | { |
18 | - /** |
|
19 | - * Removes dot segments from a path and returns the new path. |
|
20 | - * |
|
21 | - * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 |
|
22 | - */ |
|
23 | - public static function removeDotSegments(string $path): string |
|
24 | - { |
|
25 | - if ($path === '' || $path === '/') { |
|
26 | - return $path; |
|
27 | - } |
|
28 | - |
|
29 | - $results = []; |
|
30 | - $segments = explode('/', $path); |
|
31 | - foreach ($segments as $segment) { |
|
32 | - if ($segment === '..') { |
|
33 | - array_pop($results); |
|
34 | - } elseif ($segment !== '.') { |
|
35 | - $results[] = $segment; |
|
36 | - } |
|
37 | - } |
|
38 | - |
|
39 | - $newPath = implode('/', $results); |
|
40 | - |
|
41 | - if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) { |
|
42 | - // Re-add the leading slash if necessary for cases like "/.." |
|
43 | - $newPath = '/'.$newPath; |
|
44 | - } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) { |
|
45 | - // Add the trailing slash if necessary |
|
46 | - // If newPath is not empty, then $segment must be set and is the last segment from the foreach |
|
47 | - $newPath .= '/'; |
|
48 | - } |
|
49 | - |
|
50 | - return $newPath; |
|
51 | - } |
|
52 | - |
|
53 | - /** |
|
54 | - * Converts the relative URI into a new URI that is resolved against the base URI. |
|
55 | - * |
|
56 | - * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2 |
|
57 | - */ |
|
58 | - public static function resolve(UriInterface $base, UriInterface $rel): UriInterface |
|
59 | - { |
|
60 | - if ((string) $rel === '') { |
|
61 | - // we can simply return the same base URI instance for this same-document reference |
|
62 | - return $base; |
|
63 | - } |
|
64 | - |
|
65 | - if ($rel->getScheme() != '') { |
|
66 | - return $rel->withPath(self::removeDotSegments($rel->getPath())); |
|
67 | - } |
|
68 | - |
|
69 | - if ($rel->getAuthority() != '') { |
|
70 | - $targetAuthority = $rel->getAuthority(); |
|
71 | - $targetPath = self::removeDotSegments($rel->getPath()); |
|
72 | - $targetQuery = $rel->getQuery(); |
|
73 | - } else { |
|
74 | - $targetAuthority = $base->getAuthority(); |
|
75 | - if ($rel->getPath() === '') { |
|
76 | - $targetPath = $base->getPath(); |
|
77 | - $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); |
|
78 | - } else { |
|
79 | - if ($rel->getPath()[0] === '/') { |
|
80 | - $targetPath = $rel->getPath(); |
|
81 | - } else { |
|
82 | - if ($targetAuthority != '' && $base->getPath() === '') { |
|
83 | - $targetPath = '/'.$rel->getPath(); |
|
84 | - } else { |
|
85 | - $lastSlashPos = strrpos($base->getPath(), '/'); |
|
86 | - if ($lastSlashPos === false) { |
|
87 | - $targetPath = $rel->getPath(); |
|
88 | - } else { |
|
89 | - $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath(); |
|
90 | - } |
|
91 | - } |
|
92 | - } |
|
93 | - $targetPath = self::removeDotSegments($targetPath); |
|
94 | - $targetQuery = $rel->getQuery(); |
|
95 | - } |
|
96 | - } |
|
97 | - |
|
98 | - return new Uri(Uri::composeComponents( |
|
99 | - $base->getScheme(), |
|
100 | - $targetAuthority, |
|
101 | - $targetPath, |
|
102 | - $targetQuery, |
|
103 | - $rel->getFragment() |
|
104 | - )); |
|
105 | - } |
|
106 | - |
|
107 | - /** |
|
108 | - * Returns the target URI as a relative reference from the base URI. |
|
109 | - * |
|
110 | - * This method is the counterpart to resolve(): |
|
111 | - * |
|
112 | - * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) |
|
113 | - * |
|
114 | - * One use-case is to use the current request URI as base URI and then generate relative links in your documents |
|
115 | - * to reduce the document size or offer self-contained downloadable document archives. |
|
116 | - * |
|
117 | - * $base = new Uri('http://example.com/a/b/'); |
|
118 | - * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. |
|
119 | - * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. |
|
120 | - * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. |
|
121 | - * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. |
|
122 | - * |
|
123 | - * This method also accepts a target that is already relative and will try to relativize it further. Only a |
|
124 | - * relative-path reference will be returned as-is. |
|
125 | - * |
|
126 | - * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well |
|
127 | - */ |
|
128 | - public static function relativize(UriInterface $base, UriInterface $target): UriInterface |
|
129 | - { |
|
130 | - if ($target->getScheme() !== '' |
|
131 | - && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') |
|
132 | - ) { |
|
133 | - return $target; |
|
134 | - } |
|
135 | - |
|
136 | - if (Uri::isRelativePathReference($target)) { |
|
137 | - // As the target is already highly relative we return it as-is. It would be possible to resolve |
|
138 | - // the target with `$target = self::resolve($base, $target);` and then try make it more relative |
|
139 | - // by removing a duplicate query. But let's not do that automatically. |
|
140 | - return $target; |
|
141 | - } |
|
142 | - |
|
143 | - if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { |
|
144 | - return $target->withScheme(''); |
|
145 | - } |
|
146 | - |
|
147 | - // We must remove the path before removing the authority because if the path starts with two slashes, the URI |
|
148 | - // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also |
|
149 | - // invalid. |
|
150 | - $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); |
|
151 | - |
|
152 | - if ($base->getPath() !== $target->getPath()) { |
|
153 | - return $emptyPathUri->withPath(self::getRelativePath($base, $target)); |
|
154 | - } |
|
155 | - |
|
156 | - if ($base->getQuery() === $target->getQuery()) { |
|
157 | - // Only the target fragment is left. And it must be returned even if base and target fragment are the same. |
|
158 | - return $emptyPathUri->withQuery(''); |
|
159 | - } |
|
160 | - |
|
161 | - // If the base URI has a query but the target has none, we cannot return an empty path reference as it would |
|
162 | - // inherit the base query component when resolving. |
|
163 | - if ($target->getQuery() === '') { |
|
164 | - $segments = explode('/', $target->getPath()); |
|
165 | - /** @var string $lastSegment */ |
|
166 | - $lastSegment = end($segments); |
|
167 | - |
|
168 | - return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); |
|
169 | - } |
|
170 | - |
|
171 | - return $emptyPathUri; |
|
172 | - } |
|
173 | - |
|
174 | - private static function getRelativePath(UriInterface $base, UriInterface $target): string |
|
175 | - { |
|
176 | - $sourceSegments = explode('/', $base->getPath()); |
|
177 | - $targetSegments = explode('/', $target->getPath()); |
|
178 | - array_pop($sourceSegments); |
|
179 | - $targetLastSegment = array_pop($targetSegments); |
|
180 | - foreach ($sourceSegments as $i => $segment) { |
|
181 | - if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { |
|
182 | - unset($sourceSegments[$i], $targetSegments[$i]); |
|
183 | - } else { |
|
184 | - break; |
|
185 | - } |
|
186 | - } |
|
187 | - $targetSegments[] = $targetLastSegment; |
|
188 | - $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments); |
|
189 | - |
|
190 | - // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". |
|
191 | - // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used |
|
192 | - // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. |
|
193 | - if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { |
|
194 | - $relativePath = "./$relativePath"; |
|
195 | - } elseif ('/' === $relativePath[0]) { |
|
196 | - if ($base->getAuthority() != '' && $base->getPath() === '') { |
|
197 | - // In this case an extra slash is added by resolve() automatically. So we must not add one here. |
|
198 | - $relativePath = ".$relativePath"; |
|
199 | - } else { |
|
200 | - $relativePath = "./$relativePath"; |
|
201 | - } |
|
202 | - } |
|
203 | - |
|
204 | - return $relativePath; |
|
205 | - } |
|
206 | - |
|
207 | - private function __construct() |
|
208 | - { |
|
209 | - // cannot be instantiated |
|
210 | - } |
|
18 | + /** |
|
19 | + * Removes dot segments from a path and returns the new path. |
|
20 | + * |
|
21 | + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 |
|
22 | + */ |
|
23 | + public static function removeDotSegments(string $path): string |
|
24 | + { |
|
25 | + if ($path === '' || $path === '/') { |
|
26 | + return $path; |
|
27 | + } |
|
28 | + |
|
29 | + $results = []; |
|
30 | + $segments = explode('/', $path); |
|
31 | + foreach ($segments as $segment) { |
|
32 | + if ($segment === '..') { |
|
33 | + array_pop($results); |
|
34 | + } elseif ($segment !== '.') { |
|
35 | + $results[] = $segment; |
|
36 | + } |
|
37 | + } |
|
38 | + |
|
39 | + $newPath = implode('/', $results); |
|
40 | + |
|
41 | + if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) { |
|
42 | + // Re-add the leading slash if necessary for cases like "/.." |
|
43 | + $newPath = '/'.$newPath; |
|
44 | + } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) { |
|
45 | + // Add the trailing slash if necessary |
|
46 | + // If newPath is not empty, then $segment must be set and is the last segment from the foreach |
|
47 | + $newPath .= '/'; |
|
48 | + } |
|
49 | + |
|
50 | + return $newPath; |
|
51 | + } |
|
52 | + |
|
53 | + /** |
|
54 | + * Converts the relative URI into a new URI that is resolved against the base URI. |
|
55 | + * |
|
56 | + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2 |
|
57 | + */ |
|
58 | + public static function resolve(UriInterface $base, UriInterface $rel): UriInterface |
|
59 | + { |
|
60 | + if ((string) $rel === '') { |
|
61 | + // we can simply return the same base URI instance for this same-document reference |
|
62 | + return $base; |
|
63 | + } |
|
64 | + |
|
65 | + if ($rel->getScheme() != '') { |
|
66 | + return $rel->withPath(self::removeDotSegments($rel->getPath())); |
|
67 | + } |
|
68 | + |
|
69 | + if ($rel->getAuthority() != '') { |
|
70 | + $targetAuthority = $rel->getAuthority(); |
|
71 | + $targetPath = self::removeDotSegments($rel->getPath()); |
|
72 | + $targetQuery = $rel->getQuery(); |
|
73 | + } else { |
|
74 | + $targetAuthority = $base->getAuthority(); |
|
75 | + if ($rel->getPath() === '') { |
|
76 | + $targetPath = $base->getPath(); |
|
77 | + $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); |
|
78 | + } else { |
|
79 | + if ($rel->getPath()[0] === '/') { |
|
80 | + $targetPath = $rel->getPath(); |
|
81 | + } else { |
|
82 | + if ($targetAuthority != '' && $base->getPath() === '') { |
|
83 | + $targetPath = '/'.$rel->getPath(); |
|
84 | + } else { |
|
85 | + $lastSlashPos = strrpos($base->getPath(), '/'); |
|
86 | + if ($lastSlashPos === false) { |
|
87 | + $targetPath = $rel->getPath(); |
|
88 | + } else { |
|
89 | + $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath(); |
|
90 | + } |
|
91 | + } |
|
92 | + } |
|
93 | + $targetPath = self::removeDotSegments($targetPath); |
|
94 | + $targetQuery = $rel->getQuery(); |
|
95 | + } |
|
96 | + } |
|
97 | + |
|
98 | + return new Uri(Uri::composeComponents( |
|
99 | + $base->getScheme(), |
|
100 | + $targetAuthority, |
|
101 | + $targetPath, |
|
102 | + $targetQuery, |
|
103 | + $rel->getFragment() |
|
104 | + )); |
|
105 | + } |
|
106 | + |
|
107 | + /** |
|
108 | + * Returns the target URI as a relative reference from the base URI. |
|
109 | + * |
|
110 | + * This method is the counterpart to resolve(): |
|
111 | + * |
|
112 | + * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) |
|
113 | + * |
|
114 | + * One use-case is to use the current request URI as base URI and then generate relative links in your documents |
|
115 | + * to reduce the document size or offer self-contained downloadable document archives. |
|
116 | + * |
|
117 | + * $base = new Uri('http://example.com/a/b/'); |
|
118 | + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. |
|
119 | + * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. |
|
120 | + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. |
|
121 | + * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. |
|
122 | + * |
|
123 | + * This method also accepts a target that is already relative and will try to relativize it further. Only a |
|
124 | + * relative-path reference will be returned as-is. |
|
125 | + * |
|
126 | + * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well |
|
127 | + */ |
|
128 | + public static function relativize(UriInterface $base, UriInterface $target): UriInterface |
|
129 | + { |
|
130 | + if ($target->getScheme() !== '' |
|
131 | + && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') |
|
132 | + ) { |
|
133 | + return $target; |
|
134 | + } |
|
135 | + |
|
136 | + if (Uri::isRelativePathReference($target)) { |
|
137 | + // As the target is already highly relative we return it as-is. It would be possible to resolve |
|
138 | + // the target with `$target = self::resolve($base, $target);` and then try make it more relative |
|
139 | + // by removing a duplicate query. But let's not do that automatically. |
|
140 | + return $target; |
|
141 | + } |
|
142 | + |
|
143 | + if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { |
|
144 | + return $target->withScheme(''); |
|
145 | + } |
|
146 | + |
|
147 | + // We must remove the path before removing the authority because if the path starts with two slashes, the URI |
|
148 | + // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also |
|
149 | + // invalid. |
|
150 | + $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); |
|
151 | + |
|
152 | + if ($base->getPath() !== $target->getPath()) { |
|
153 | + return $emptyPathUri->withPath(self::getRelativePath($base, $target)); |
|
154 | + } |
|
155 | + |
|
156 | + if ($base->getQuery() === $target->getQuery()) { |
|
157 | + // Only the target fragment is left. And it must be returned even if base and target fragment are the same. |
|
158 | + return $emptyPathUri->withQuery(''); |
|
159 | + } |
|
160 | + |
|
161 | + // If the base URI has a query but the target has none, we cannot return an empty path reference as it would |
|
162 | + // inherit the base query component when resolving. |
|
163 | + if ($target->getQuery() === '') { |
|
164 | + $segments = explode('/', $target->getPath()); |
|
165 | + /** @var string $lastSegment */ |
|
166 | + $lastSegment = end($segments); |
|
167 | + |
|
168 | + return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); |
|
169 | + } |
|
170 | + |
|
171 | + return $emptyPathUri; |
|
172 | + } |
|
173 | + |
|
174 | + private static function getRelativePath(UriInterface $base, UriInterface $target): string |
|
175 | + { |
|
176 | + $sourceSegments = explode('/', $base->getPath()); |
|
177 | + $targetSegments = explode('/', $target->getPath()); |
|
178 | + array_pop($sourceSegments); |
|
179 | + $targetLastSegment = array_pop($targetSegments); |
|
180 | + foreach ($sourceSegments as $i => $segment) { |
|
181 | + if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { |
|
182 | + unset($sourceSegments[$i], $targetSegments[$i]); |
|
183 | + } else { |
|
184 | + break; |
|
185 | + } |
|
186 | + } |
|
187 | + $targetSegments[] = $targetLastSegment; |
|
188 | + $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments); |
|
189 | + |
|
190 | + // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". |
|
191 | + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used |
|
192 | + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. |
|
193 | + if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { |
|
194 | + $relativePath = "./$relativePath"; |
|
195 | + } elseif ('/' === $relativePath[0]) { |
|
196 | + if ($base->getAuthority() != '' && $base->getPath() === '') { |
|
197 | + // In this case an extra slash is added by resolve() automatically. So we must not add one here. |
|
198 | + $relativePath = ".$relativePath"; |
|
199 | + } else { |
|
200 | + $relativePath = "./$relativePath"; |
|
201 | + } |
|
202 | + } |
|
203 | + |
|
204 | + return $relativePath; |
|
205 | + } |
|
206 | + |
|
207 | + private function __construct() |
|
208 | + { |
|
209 | + // cannot be instantiated |
|
210 | + } |
|
211 | 211 | } |
@@ -57,7 +57,7 @@ |
||
57 | 57 | */ |
58 | 58 | public static function resolve(UriInterface $base, UriInterface $rel): UriInterface |
59 | 59 | { |
60 | - if ((string) $rel === '') { |
|
60 | + if ((string)$rel === '') { |
|
61 | 61 | // we can simply return the same base URI instance for this same-document reference |
62 | 62 | return $base; |
63 | 63 | } |
@@ -13,8 +13,7 @@ |
||
13 | 13 | * |
14 | 14 | * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5 |
15 | 15 | */ |
16 | -final class UriResolver |
|
17 | -{ |
|
16 | +final class UriResolver { |
|
18 | 17 | /** |
19 | 18 | * Removes dot segments from a path and returns the new path. |
20 | 19 | * |
@@ -6,108 +6,108 @@ |
||
6 | 6 | |
7 | 7 | final class Query |
8 | 8 | { |
9 | - /** |
|
10 | - * Parse a query string into an associative array. |
|
11 | - * |
|
12 | - * If multiple values are found for the same key, the value of that key |
|
13 | - * value pair will become an array. This function does not parse nested |
|
14 | - * PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2` |
|
15 | - * will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`. |
|
16 | - * |
|
17 | - * @param string $str Query string to parse |
|
18 | - * @param int|bool $urlEncoding How the query string is encoded |
|
19 | - */ |
|
20 | - public static function parse(string $str, $urlEncoding = true): array |
|
21 | - { |
|
22 | - $result = []; |
|
9 | + /** |
|
10 | + * Parse a query string into an associative array. |
|
11 | + * |
|
12 | + * If multiple values are found for the same key, the value of that key |
|
13 | + * value pair will become an array. This function does not parse nested |
|
14 | + * PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2` |
|
15 | + * will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`. |
|
16 | + * |
|
17 | + * @param string $str Query string to parse |
|
18 | + * @param int|bool $urlEncoding How the query string is encoded |
|
19 | + */ |
|
20 | + public static function parse(string $str, $urlEncoding = true): array |
|
21 | + { |
|
22 | + $result = []; |
|
23 | 23 | |
24 | - if ($str === '') { |
|
25 | - return $result; |
|
26 | - } |
|
24 | + if ($str === '') { |
|
25 | + return $result; |
|
26 | + } |
|
27 | 27 | |
28 | - if ($urlEncoding === true) { |
|
29 | - $decoder = function ($value) { |
|
30 | - return rawurldecode(str_replace('+', ' ', (string) $value)); |
|
31 | - }; |
|
32 | - } elseif ($urlEncoding === PHP_QUERY_RFC3986) { |
|
33 | - $decoder = 'rawurldecode'; |
|
34 | - } elseif ($urlEncoding === PHP_QUERY_RFC1738) { |
|
35 | - $decoder = 'urldecode'; |
|
36 | - } else { |
|
37 | - $decoder = function ($str) { |
|
38 | - return $str; |
|
39 | - }; |
|
40 | - } |
|
28 | + if ($urlEncoding === true) { |
|
29 | + $decoder = function ($value) { |
|
30 | + return rawurldecode(str_replace('+', ' ', (string) $value)); |
|
31 | + }; |
|
32 | + } elseif ($urlEncoding === PHP_QUERY_RFC3986) { |
|
33 | + $decoder = 'rawurldecode'; |
|
34 | + } elseif ($urlEncoding === PHP_QUERY_RFC1738) { |
|
35 | + $decoder = 'urldecode'; |
|
36 | + } else { |
|
37 | + $decoder = function ($str) { |
|
38 | + return $str; |
|
39 | + }; |
|
40 | + } |
|
41 | 41 | |
42 | - foreach (explode('&', $str) as $kvp) { |
|
43 | - $parts = explode('=', $kvp, 2); |
|
44 | - $key = $decoder($parts[0]); |
|
45 | - $value = isset($parts[1]) ? $decoder($parts[1]) : null; |
|
46 | - if (!array_key_exists($key, $result)) { |
|
47 | - $result[$key] = $value; |
|
48 | - } else { |
|
49 | - if (!is_array($result[$key])) { |
|
50 | - $result[$key] = [$result[$key]]; |
|
51 | - } |
|
52 | - $result[$key][] = $value; |
|
53 | - } |
|
54 | - } |
|
42 | + foreach (explode('&', $str) as $kvp) { |
|
43 | + $parts = explode('=', $kvp, 2); |
|
44 | + $key = $decoder($parts[0]); |
|
45 | + $value = isset($parts[1]) ? $decoder($parts[1]) : null; |
|
46 | + if (!array_key_exists($key, $result)) { |
|
47 | + $result[$key] = $value; |
|
48 | + } else { |
|
49 | + if (!is_array($result[$key])) { |
|
50 | + $result[$key] = [$result[$key]]; |
|
51 | + } |
|
52 | + $result[$key][] = $value; |
|
53 | + } |
|
54 | + } |
|
55 | 55 | |
56 | - return $result; |
|
57 | - } |
|
56 | + return $result; |
|
57 | + } |
|
58 | 58 | |
59 | - /** |
|
60 | - * Build a query string from an array of key value pairs. |
|
61 | - * |
|
62 | - * This function can use the return value of `parse()` to build a query |
|
63 | - * string. This function does not modify the provided keys when an array is |
|
64 | - * encountered (like `http_build_query()` would). |
|
65 | - * |
|
66 | - * @param array $params Query string parameters. |
|
67 | - * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 |
|
68 | - * to encode using RFC3986, or PHP_QUERY_RFC1738 |
|
69 | - * to encode using RFC1738. |
|
70 | - */ |
|
71 | - public static function build(array $params, $encoding = PHP_QUERY_RFC3986): string |
|
72 | - { |
|
73 | - if (!$params) { |
|
74 | - return ''; |
|
75 | - } |
|
59 | + /** |
|
60 | + * Build a query string from an array of key value pairs. |
|
61 | + * |
|
62 | + * This function can use the return value of `parse()` to build a query |
|
63 | + * string. This function does not modify the provided keys when an array is |
|
64 | + * encountered (like `http_build_query()` would). |
|
65 | + * |
|
66 | + * @param array $params Query string parameters. |
|
67 | + * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 |
|
68 | + * to encode using RFC3986, or PHP_QUERY_RFC1738 |
|
69 | + * to encode using RFC1738. |
|
70 | + */ |
|
71 | + public static function build(array $params, $encoding = PHP_QUERY_RFC3986): string |
|
72 | + { |
|
73 | + if (!$params) { |
|
74 | + return ''; |
|
75 | + } |
|
76 | 76 | |
77 | - if ($encoding === false) { |
|
78 | - $encoder = function (string $str): string { |
|
79 | - return $str; |
|
80 | - }; |
|
81 | - } elseif ($encoding === PHP_QUERY_RFC3986) { |
|
82 | - $encoder = 'rawurlencode'; |
|
83 | - } elseif ($encoding === PHP_QUERY_RFC1738) { |
|
84 | - $encoder = 'urlencode'; |
|
85 | - } else { |
|
86 | - throw new \InvalidArgumentException('Invalid type'); |
|
87 | - } |
|
77 | + if ($encoding === false) { |
|
78 | + $encoder = function (string $str): string { |
|
79 | + return $str; |
|
80 | + }; |
|
81 | + } elseif ($encoding === PHP_QUERY_RFC3986) { |
|
82 | + $encoder = 'rawurlencode'; |
|
83 | + } elseif ($encoding === PHP_QUERY_RFC1738) { |
|
84 | + $encoder = 'urlencode'; |
|
85 | + } else { |
|
86 | + throw new \InvalidArgumentException('Invalid type'); |
|
87 | + } |
|
88 | 88 | |
89 | - $qs = ''; |
|
90 | - foreach ($params as $k => $v) { |
|
91 | - $k = $encoder((string) $k); |
|
92 | - if (!is_array($v)) { |
|
93 | - $qs .= $k; |
|
94 | - $v = is_bool($v) ? (int) $v : $v; |
|
95 | - if ($v !== null) { |
|
96 | - $qs .= '='.$encoder((string) $v); |
|
97 | - } |
|
98 | - $qs .= '&'; |
|
99 | - } else { |
|
100 | - foreach ($v as $vv) { |
|
101 | - $qs .= $k; |
|
102 | - $vv = is_bool($vv) ? (int) $vv : $vv; |
|
103 | - if ($vv !== null) { |
|
104 | - $qs .= '='.$encoder((string) $vv); |
|
105 | - } |
|
106 | - $qs .= '&'; |
|
107 | - } |
|
108 | - } |
|
109 | - } |
|
89 | + $qs = ''; |
|
90 | + foreach ($params as $k => $v) { |
|
91 | + $k = $encoder((string) $k); |
|
92 | + if (!is_array($v)) { |
|
93 | + $qs .= $k; |
|
94 | + $v = is_bool($v) ? (int) $v : $v; |
|
95 | + if ($v !== null) { |
|
96 | + $qs .= '='.$encoder((string) $v); |
|
97 | + } |
|
98 | + $qs .= '&'; |
|
99 | + } else { |
|
100 | + foreach ($v as $vv) { |
|
101 | + $qs .= $k; |
|
102 | + $vv = is_bool($vv) ? (int) $vv : $vv; |
|
103 | + if ($vv !== null) { |
|
104 | + $qs .= '='.$encoder((string) $vv); |
|
105 | + } |
|
106 | + $qs .= '&'; |
|
107 | + } |
|
108 | + } |
|
109 | + } |
|
110 | 110 | |
111 | - return $qs ? (string) substr($qs, 0, -1) : ''; |
|
112 | - } |
|
111 | + return $qs ? (string) substr($qs, 0, -1) : ''; |
|
112 | + } |
|
113 | 113 | } |
@@ -26,15 +26,15 @@ discard block |
||
26 | 26 | } |
27 | 27 | |
28 | 28 | if ($urlEncoding === true) { |
29 | - $decoder = function ($value) { |
|
30 | - return rawurldecode(str_replace('+', ' ', (string) $value)); |
|
29 | + $decoder = function($value) { |
|
30 | + return rawurldecode(str_replace('+', ' ', (string)$value)); |
|
31 | 31 | }; |
32 | 32 | } elseif ($urlEncoding === PHP_QUERY_RFC3986) { |
33 | 33 | $decoder = 'rawurldecode'; |
34 | 34 | } elseif ($urlEncoding === PHP_QUERY_RFC1738) { |
35 | 35 | $decoder = 'urldecode'; |
36 | 36 | } else { |
37 | - $decoder = function ($str) { |
|
37 | + $decoder = function($str) { |
|
38 | 38 | return $str; |
39 | 39 | }; |
40 | 40 | } |
@@ -75,7 +75,7 @@ discard block |
||
75 | 75 | } |
76 | 76 | |
77 | 77 | if ($encoding === false) { |
78 | - $encoder = function (string $str): string { |
|
78 | + $encoder = function(string $str): string { |
|
79 | 79 | return $str; |
80 | 80 | }; |
81 | 81 | } elseif ($encoding === PHP_QUERY_RFC3986) { |
@@ -88,26 +88,26 @@ discard block |
||
88 | 88 | |
89 | 89 | $qs = ''; |
90 | 90 | foreach ($params as $k => $v) { |
91 | - $k = $encoder((string) $k); |
|
91 | + $k = $encoder((string)$k); |
|
92 | 92 | if (!is_array($v)) { |
93 | 93 | $qs .= $k; |
94 | - $v = is_bool($v) ? (int) $v : $v; |
|
94 | + $v = is_bool($v) ? (int)$v : $v; |
|
95 | 95 | if ($v !== null) { |
96 | - $qs .= '='.$encoder((string) $v); |
|
96 | + $qs .= '='.$encoder((string)$v); |
|
97 | 97 | } |
98 | 98 | $qs .= '&'; |
99 | 99 | } else { |
100 | 100 | foreach ($v as $vv) { |
101 | 101 | $qs .= $k; |
102 | - $vv = is_bool($vv) ? (int) $vv : $vv; |
|
102 | + $vv = is_bool($vv) ? (int)$vv : $vv; |
|
103 | 103 | if ($vv !== null) { |
104 | - $qs .= '='.$encoder((string) $vv); |
|
104 | + $qs .= '='.$encoder((string)$vv); |
|
105 | 105 | } |
106 | 106 | $qs .= '&'; |
107 | 107 | } |
108 | 108 | } |
109 | 109 | } |
110 | 110 | |
111 | - return $qs ? (string) substr($qs, 0, -1) : ''; |
|
111 | + return $qs ? (string)substr($qs, 0, -1) : ''; |
|
112 | 112 | } |
113 | 113 | } |
@@ -4,8 +4,7 @@ |
||
4 | 4 | |
5 | 5 | namespace OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7; |
6 | 6 | |
7 | -final class Query |
|
8 | -{ |
|
7 | +final class Query { |
|
9 | 8 | /** |
10 | 9 | * Parse a query string into an associative array. |
11 | 10 | * |
@@ -26,315 +26,315 @@ |
||
26 | 26 | */ |
27 | 27 | class ServerRequest extends Request implements ServerRequestInterface |
28 | 28 | { |
29 | - /** |
|
30 | - * @var array |
|
31 | - */ |
|
32 | - private $attributes = []; |
|
33 | - |
|
34 | - /** |
|
35 | - * @var array |
|
36 | - */ |
|
37 | - private $cookieParams = []; |
|
38 | - |
|
39 | - /** |
|
40 | - * @var array|object|null |
|
41 | - */ |
|
42 | - private $parsedBody; |
|
43 | - |
|
44 | - /** |
|
45 | - * @var array |
|
46 | - */ |
|
47 | - private $queryParams = []; |
|
48 | - |
|
49 | - /** |
|
50 | - * @var array |
|
51 | - */ |
|
52 | - private $serverParams; |
|
53 | - |
|
54 | - /** |
|
55 | - * @var array |
|
56 | - */ |
|
57 | - private $uploadedFiles = []; |
|
58 | - |
|
59 | - /** |
|
60 | - * @param string $method HTTP method |
|
61 | - * @param string|UriInterface $uri URI |
|
62 | - * @param (string|string[])[] $headers Request headers |
|
63 | - * @param string|resource|StreamInterface|null $body Request body |
|
64 | - * @param string $version Protocol version |
|
65 | - * @param array $serverParams Typically the $_SERVER superglobal |
|
66 | - */ |
|
67 | - public function __construct( |
|
68 | - string $method, |
|
69 | - $uri, |
|
70 | - array $headers = [], |
|
71 | - $body = null, |
|
72 | - string $version = '1.1', |
|
73 | - array $serverParams = [] |
|
74 | - ) { |
|
75 | - $this->serverParams = $serverParams; |
|
76 | - |
|
77 | - parent::__construct($method, $uri, $headers, $body, $version); |
|
78 | - } |
|
79 | - |
|
80 | - /** |
|
81 | - * Return an UploadedFile instance array. |
|
82 | - * |
|
83 | - * @param array $files An array which respect $_FILES structure |
|
84 | - * |
|
85 | - * @throws InvalidArgumentException for unrecognized values |
|
86 | - */ |
|
87 | - public static function normalizeFiles(array $files): array |
|
88 | - { |
|
89 | - $normalized = []; |
|
90 | - |
|
91 | - foreach ($files as $key => $value) { |
|
92 | - if ($value instanceof UploadedFileInterface) { |
|
93 | - $normalized[$key] = $value; |
|
94 | - } elseif (is_array($value) && isset($value['tmp_name'])) { |
|
95 | - $normalized[$key] = self::createUploadedFileFromSpec($value); |
|
96 | - } elseif (is_array($value)) { |
|
97 | - $normalized[$key] = self::normalizeFiles($value); |
|
98 | - continue; |
|
99 | - } else { |
|
100 | - throw new InvalidArgumentException('Invalid value in files specification'); |
|
101 | - } |
|
102 | - } |
|
103 | - |
|
104 | - return $normalized; |
|
105 | - } |
|
106 | - |
|
107 | - /** |
|
108 | - * Create and return an UploadedFile instance from a $_FILES specification. |
|
109 | - * |
|
110 | - * If the specification represents an array of values, this method will |
|
111 | - * delegate to normalizeNestedFileSpec() and return that return value. |
|
112 | - * |
|
113 | - * @param array $value $_FILES struct |
|
114 | - * |
|
115 | - * @return UploadedFileInterface|UploadedFileInterface[] |
|
116 | - */ |
|
117 | - private static function createUploadedFileFromSpec(array $value) |
|
118 | - { |
|
119 | - if (is_array($value['tmp_name'])) { |
|
120 | - return self::normalizeNestedFileSpec($value); |
|
121 | - } |
|
122 | - |
|
123 | - return new UploadedFile( |
|
124 | - $value['tmp_name'], |
|
125 | - (int) $value['size'], |
|
126 | - (int) $value['error'], |
|
127 | - $value['name'], |
|
128 | - $value['type'] |
|
129 | - ); |
|
130 | - } |
|
131 | - |
|
132 | - /** |
|
133 | - * Normalize an array of file specifications. |
|
134 | - * |
|
135 | - * Loops through all nested files and returns a normalized array of |
|
136 | - * UploadedFileInterface instances. |
|
137 | - * |
|
138 | - * @return UploadedFileInterface[] |
|
139 | - */ |
|
140 | - private static function normalizeNestedFileSpec(array $files = []): array |
|
141 | - { |
|
142 | - $normalizedFiles = []; |
|
143 | - |
|
144 | - foreach (array_keys($files['tmp_name']) as $key) { |
|
145 | - $spec = [ |
|
146 | - 'tmp_name' => $files['tmp_name'][$key], |
|
147 | - 'size' => $files['size'][$key] ?? null, |
|
148 | - 'error' => $files['error'][$key] ?? null, |
|
149 | - 'name' => $files['name'][$key] ?? null, |
|
150 | - 'type' => $files['type'][$key] ?? null, |
|
151 | - ]; |
|
152 | - $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); |
|
153 | - } |
|
154 | - |
|
155 | - return $normalizedFiles; |
|
156 | - } |
|
157 | - |
|
158 | - /** |
|
159 | - * Return a ServerRequest populated with superglobals: |
|
160 | - * $_GET |
|
161 | - * $_POST |
|
162 | - * $_COOKIE |
|
163 | - * $_FILES |
|
164 | - * $_SERVER |
|
165 | - */ |
|
166 | - public static function fromGlobals(): ServerRequestInterface |
|
167 | - { |
|
168 | - $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; |
|
169 | - $headers = getallheaders(); |
|
170 | - $uri = self::getUriFromGlobals(); |
|
171 | - $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); |
|
172 | - $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; |
|
173 | - |
|
174 | - $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); |
|
175 | - |
|
176 | - return $serverRequest |
|
177 | - ->withCookieParams($_COOKIE) |
|
178 | - ->withQueryParams($_GET) |
|
179 | - ->withParsedBody($_POST) |
|
180 | - ->withUploadedFiles(self::normalizeFiles($_FILES)); |
|
181 | - } |
|
182 | - |
|
183 | - private static function extractHostAndPortFromAuthority(string $authority): array |
|
184 | - { |
|
185 | - $uri = 'http://'.$authority; |
|
186 | - $parts = parse_url($uri); |
|
187 | - if (false === $parts) { |
|
188 | - return [null, null]; |
|
189 | - } |
|
190 | - |
|
191 | - $host = $parts['host'] ?? null; |
|
192 | - $port = $parts['port'] ?? null; |
|
193 | - |
|
194 | - return [$host, $port]; |
|
195 | - } |
|
196 | - |
|
197 | - /** |
|
198 | - * Get a Uri populated with values from $_SERVER. |
|
199 | - */ |
|
200 | - public static function getUriFromGlobals(): UriInterface |
|
201 | - { |
|
202 | - $uri = new Uri(''); |
|
203 | - |
|
204 | - $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); |
|
205 | - |
|
206 | - $hasPort = false; |
|
207 | - if (isset($_SERVER['HTTP_HOST'])) { |
|
208 | - [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); |
|
209 | - if ($host !== null) { |
|
210 | - $uri = $uri->withHost($host); |
|
211 | - } |
|
212 | - |
|
213 | - if ($port !== null) { |
|
214 | - $hasPort = true; |
|
215 | - $uri = $uri->withPort($port); |
|
216 | - } |
|
217 | - } elseif (isset($_SERVER['SERVER_NAME'])) { |
|
218 | - $uri = $uri->withHost($_SERVER['SERVER_NAME']); |
|
219 | - } elseif (isset($_SERVER['SERVER_ADDR'])) { |
|
220 | - $uri = $uri->withHost($_SERVER['SERVER_ADDR']); |
|
221 | - } |
|
222 | - |
|
223 | - if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { |
|
224 | - $uri = $uri->withPort($_SERVER['SERVER_PORT']); |
|
225 | - } |
|
226 | - |
|
227 | - $hasQuery = false; |
|
228 | - if (isset($_SERVER['REQUEST_URI'])) { |
|
229 | - $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); |
|
230 | - $uri = $uri->withPath($requestUriParts[0]); |
|
231 | - if (isset($requestUriParts[1])) { |
|
232 | - $hasQuery = true; |
|
233 | - $uri = $uri->withQuery($requestUriParts[1]); |
|
234 | - } |
|
235 | - } |
|
236 | - |
|
237 | - if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { |
|
238 | - $uri = $uri->withQuery($_SERVER['QUERY_STRING']); |
|
239 | - } |
|
240 | - |
|
241 | - return $uri; |
|
242 | - } |
|
243 | - |
|
244 | - public function getServerParams(): array |
|
245 | - { |
|
246 | - return $this->serverParams; |
|
247 | - } |
|
248 | - |
|
249 | - public function getUploadedFiles(): array |
|
250 | - { |
|
251 | - return $this->uploadedFiles; |
|
252 | - } |
|
253 | - |
|
254 | - public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface |
|
255 | - { |
|
256 | - $new = clone $this; |
|
257 | - $new->uploadedFiles = $uploadedFiles; |
|
258 | - |
|
259 | - return $new; |
|
260 | - } |
|
261 | - |
|
262 | - public function getCookieParams(): array |
|
263 | - { |
|
264 | - return $this->cookieParams; |
|
265 | - } |
|
266 | - |
|
267 | - public function withCookieParams(array $cookies): ServerRequestInterface |
|
268 | - { |
|
269 | - $new = clone $this; |
|
270 | - $new->cookieParams = $cookies; |
|
271 | - |
|
272 | - return $new; |
|
273 | - } |
|
274 | - |
|
275 | - public function getQueryParams(): array |
|
276 | - { |
|
277 | - return $this->queryParams; |
|
278 | - } |
|
279 | - |
|
280 | - public function withQueryParams(array $query): ServerRequestInterface |
|
281 | - { |
|
282 | - $new = clone $this; |
|
283 | - $new->queryParams = $query; |
|
284 | - |
|
285 | - return $new; |
|
286 | - } |
|
287 | - |
|
288 | - /** |
|
289 | - * @return array|object|null |
|
290 | - */ |
|
291 | - public function getParsedBody() |
|
292 | - { |
|
293 | - return $this->parsedBody; |
|
294 | - } |
|
295 | - |
|
296 | - public function withParsedBody($data): ServerRequestInterface |
|
297 | - { |
|
298 | - $new = clone $this; |
|
299 | - $new->parsedBody = $data; |
|
300 | - |
|
301 | - return $new; |
|
302 | - } |
|
303 | - |
|
304 | - public function getAttributes(): array |
|
305 | - { |
|
306 | - return $this->attributes; |
|
307 | - } |
|
308 | - |
|
309 | - /** |
|
310 | - * @return mixed |
|
311 | - */ |
|
312 | - public function getAttribute($attribute, $default = null) |
|
313 | - { |
|
314 | - if (false === array_key_exists($attribute, $this->attributes)) { |
|
315 | - return $default; |
|
316 | - } |
|
317 | - |
|
318 | - return $this->attributes[$attribute]; |
|
319 | - } |
|
320 | - |
|
321 | - public function withAttribute($attribute, $value): ServerRequestInterface |
|
322 | - { |
|
323 | - $new = clone $this; |
|
324 | - $new->attributes[$attribute] = $value; |
|
325 | - |
|
326 | - return $new; |
|
327 | - } |
|
328 | - |
|
329 | - public function withoutAttribute($attribute): ServerRequestInterface |
|
330 | - { |
|
331 | - if (false === array_key_exists($attribute, $this->attributes)) { |
|
332 | - return $this; |
|
333 | - } |
|
334 | - |
|
335 | - $new = clone $this; |
|
336 | - unset($new->attributes[$attribute]); |
|
337 | - |
|
338 | - return $new; |
|
339 | - } |
|
29 | + /** |
|
30 | + * @var array |
|
31 | + */ |
|
32 | + private $attributes = []; |
|
33 | + |
|
34 | + /** |
|
35 | + * @var array |
|
36 | + */ |
|
37 | + private $cookieParams = []; |
|
38 | + |
|
39 | + /** |
|
40 | + * @var array|object|null |
|
41 | + */ |
|
42 | + private $parsedBody; |
|
43 | + |
|
44 | + /** |
|
45 | + * @var array |
|
46 | + */ |
|
47 | + private $queryParams = []; |
|
48 | + |
|
49 | + /** |
|
50 | + * @var array |
|
51 | + */ |
|
52 | + private $serverParams; |
|
53 | + |
|
54 | + /** |
|
55 | + * @var array |
|
56 | + */ |
|
57 | + private $uploadedFiles = []; |
|
58 | + |
|
59 | + /** |
|
60 | + * @param string $method HTTP method |
|
61 | + * @param string|UriInterface $uri URI |
|
62 | + * @param (string|string[])[] $headers Request headers |
|
63 | + * @param string|resource|StreamInterface|null $body Request body |
|
64 | + * @param string $version Protocol version |
|
65 | + * @param array $serverParams Typically the $_SERVER superglobal |
|
66 | + */ |
|
67 | + public function __construct( |
|
68 | + string $method, |
|
69 | + $uri, |
|
70 | + array $headers = [], |
|
71 | + $body = null, |
|
72 | + string $version = '1.1', |
|
73 | + array $serverParams = [] |
|
74 | + ) { |
|
75 | + $this->serverParams = $serverParams; |
|
76 | + |
|
77 | + parent::__construct($method, $uri, $headers, $body, $version); |
|
78 | + } |
|
79 | + |
|
80 | + /** |
|
81 | + * Return an UploadedFile instance array. |
|
82 | + * |
|
83 | + * @param array $files An array which respect $_FILES structure |
|
84 | + * |
|
85 | + * @throws InvalidArgumentException for unrecognized values |
|
86 | + */ |
|
87 | + public static function normalizeFiles(array $files): array |
|
88 | + { |
|
89 | + $normalized = []; |
|
90 | + |
|
91 | + foreach ($files as $key => $value) { |
|
92 | + if ($value instanceof UploadedFileInterface) { |
|
93 | + $normalized[$key] = $value; |
|
94 | + } elseif (is_array($value) && isset($value['tmp_name'])) { |
|
95 | + $normalized[$key] = self::createUploadedFileFromSpec($value); |
|
96 | + } elseif (is_array($value)) { |
|
97 | + $normalized[$key] = self::normalizeFiles($value); |
|
98 | + continue; |
|
99 | + } else { |
|
100 | + throw new InvalidArgumentException('Invalid value in files specification'); |
|
101 | + } |
|
102 | + } |
|
103 | + |
|
104 | + return $normalized; |
|
105 | + } |
|
106 | + |
|
107 | + /** |
|
108 | + * Create and return an UploadedFile instance from a $_FILES specification. |
|
109 | + * |
|
110 | + * If the specification represents an array of values, this method will |
|
111 | + * delegate to normalizeNestedFileSpec() and return that return value. |
|
112 | + * |
|
113 | + * @param array $value $_FILES struct |
|
114 | + * |
|
115 | + * @return UploadedFileInterface|UploadedFileInterface[] |
|
116 | + */ |
|
117 | + private static function createUploadedFileFromSpec(array $value) |
|
118 | + { |
|
119 | + if (is_array($value['tmp_name'])) { |
|
120 | + return self::normalizeNestedFileSpec($value); |
|
121 | + } |
|
122 | + |
|
123 | + return new UploadedFile( |
|
124 | + $value['tmp_name'], |
|
125 | + (int) $value['size'], |
|
126 | + (int) $value['error'], |
|
127 | + $value['name'], |
|
128 | + $value['type'] |
|
129 | + ); |
|
130 | + } |
|
131 | + |
|
132 | + /** |
|
133 | + * Normalize an array of file specifications. |
|
134 | + * |
|
135 | + * Loops through all nested files and returns a normalized array of |
|
136 | + * UploadedFileInterface instances. |
|
137 | + * |
|
138 | + * @return UploadedFileInterface[] |
|
139 | + */ |
|
140 | + private static function normalizeNestedFileSpec(array $files = []): array |
|
141 | + { |
|
142 | + $normalizedFiles = []; |
|
143 | + |
|
144 | + foreach (array_keys($files['tmp_name']) as $key) { |
|
145 | + $spec = [ |
|
146 | + 'tmp_name' => $files['tmp_name'][$key], |
|
147 | + 'size' => $files['size'][$key] ?? null, |
|
148 | + 'error' => $files['error'][$key] ?? null, |
|
149 | + 'name' => $files['name'][$key] ?? null, |
|
150 | + 'type' => $files['type'][$key] ?? null, |
|
151 | + ]; |
|
152 | + $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); |
|
153 | + } |
|
154 | + |
|
155 | + return $normalizedFiles; |
|
156 | + } |
|
157 | + |
|
158 | + /** |
|
159 | + * Return a ServerRequest populated with superglobals: |
|
160 | + * $_GET |
|
161 | + * $_POST |
|
162 | + * $_COOKIE |
|
163 | + * $_FILES |
|
164 | + * $_SERVER |
|
165 | + */ |
|
166 | + public static function fromGlobals(): ServerRequestInterface |
|
167 | + { |
|
168 | + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; |
|
169 | + $headers = getallheaders(); |
|
170 | + $uri = self::getUriFromGlobals(); |
|
171 | + $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); |
|
172 | + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; |
|
173 | + |
|
174 | + $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); |
|
175 | + |
|
176 | + return $serverRequest |
|
177 | + ->withCookieParams($_COOKIE) |
|
178 | + ->withQueryParams($_GET) |
|
179 | + ->withParsedBody($_POST) |
|
180 | + ->withUploadedFiles(self::normalizeFiles($_FILES)); |
|
181 | + } |
|
182 | + |
|
183 | + private static function extractHostAndPortFromAuthority(string $authority): array |
|
184 | + { |
|
185 | + $uri = 'http://'.$authority; |
|
186 | + $parts = parse_url($uri); |
|
187 | + if (false === $parts) { |
|
188 | + return [null, null]; |
|
189 | + } |
|
190 | + |
|
191 | + $host = $parts['host'] ?? null; |
|
192 | + $port = $parts['port'] ?? null; |
|
193 | + |
|
194 | + return [$host, $port]; |
|
195 | + } |
|
196 | + |
|
197 | + /** |
|
198 | + * Get a Uri populated with values from $_SERVER. |
|
199 | + */ |
|
200 | + public static function getUriFromGlobals(): UriInterface |
|
201 | + { |
|
202 | + $uri = new Uri(''); |
|
203 | + |
|
204 | + $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); |
|
205 | + |
|
206 | + $hasPort = false; |
|
207 | + if (isset($_SERVER['HTTP_HOST'])) { |
|
208 | + [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); |
|
209 | + if ($host !== null) { |
|
210 | + $uri = $uri->withHost($host); |
|
211 | + } |
|
212 | + |
|
213 | + if ($port !== null) { |
|
214 | + $hasPort = true; |
|
215 | + $uri = $uri->withPort($port); |
|
216 | + } |
|
217 | + } elseif (isset($_SERVER['SERVER_NAME'])) { |
|
218 | + $uri = $uri->withHost($_SERVER['SERVER_NAME']); |
|
219 | + } elseif (isset($_SERVER['SERVER_ADDR'])) { |
|
220 | + $uri = $uri->withHost($_SERVER['SERVER_ADDR']); |
|
221 | + } |
|
222 | + |
|
223 | + if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { |
|
224 | + $uri = $uri->withPort($_SERVER['SERVER_PORT']); |
|
225 | + } |
|
226 | + |
|
227 | + $hasQuery = false; |
|
228 | + if (isset($_SERVER['REQUEST_URI'])) { |
|
229 | + $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); |
|
230 | + $uri = $uri->withPath($requestUriParts[0]); |
|
231 | + if (isset($requestUriParts[1])) { |
|
232 | + $hasQuery = true; |
|
233 | + $uri = $uri->withQuery($requestUriParts[1]); |
|
234 | + } |
|
235 | + } |
|
236 | + |
|
237 | + if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { |
|
238 | + $uri = $uri->withQuery($_SERVER['QUERY_STRING']); |
|
239 | + } |
|
240 | + |
|
241 | + return $uri; |
|
242 | + } |
|
243 | + |
|
244 | + public function getServerParams(): array |
|
245 | + { |
|
246 | + return $this->serverParams; |
|
247 | + } |
|
248 | + |
|
249 | + public function getUploadedFiles(): array |
|
250 | + { |
|
251 | + return $this->uploadedFiles; |
|
252 | + } |
|
253 | + |
|
254 | + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface |
|
255 | + { |
|
256 | + $new = clone $this; |
|
257 | + $new->uploadedFiles = $uploadedFiles; |
|
258 | + |
|
259 | + return $new; |
|
260 | + } |
|
261 | + |
|
262 | + public function getCookieParams(): array |
|
263 | + { |
|
264 | + return $this->cookieParams; |
|
265 | + } |
|
266 | + |
|
267 | + public function withCookieParams(array $cookies): ServerRequestInterface |
|
268 | + { |
|
269 | + $new = clone $this; |
|
270 | + $new->cookieParams = $cookies; |
|
271 | + |
|
272 | + return $new; |
|
273 | + } |
|
274 | + |
|
275 | + public function getQueryParams(): array |
|
276 | + { |
|
277 | + return $this->queryParams; |
|
278 | + } |
|
279 | + |
|
280 | + public function withQueryParams(array $query): ServerRequestInterface |
|
281 | + { |
|
282 | + $new = clone $this; |
|
283 | + $new->queryParams = $query; |
|
284 | + |
|
285 | + return $new; |
|
286 | + } |
|
287 | + |
|
288 | + /** |
|
289 | + * @return array|object|null |
|
290 | + */ |
|
291 | + public function getParsedBody() |
|
292 | + { |
|
293 | + return $this->parsedBody; |
|
294 | + } |
|
295 | + |
|
296 | + public function withParsedBody($data): ServerRequestInterface |
|
297 | + { |
|
298 | + $new = clone $this; |
|
299 | + $new->parsedBody = $data; |
|
300 | + |
|
301 | + return $new; |
|
302 | + } |
|
303 | + |
|
304 | + public function getAttributes(): array |
|
305 | + { |
|
306 | + return $this->attributes; |
|
307 | + } |
|
308 | + |
|
309 | + /** |
|
310 | + * @return mixed |
|
311 | + */ |
|
312 | + public function getAttribute($attribute, $default = null) |
|
313 | + { |
|
314 | + if (false === array_key_exists($attribute, $this->attributes)) { |
|
315 | + return $default; |
|
316 | + } |
|
317 | + |
|
318 | + return $this->attributes[$attribute]; |
|
319 | + } |
|
320 | + |
|
321 | + public function withAttribute($attribute, $value): ServerRequestInterface |
|
322 | + { |
|
323 | + $new = clone $this; |
|
324 | + $new->attributes[$attribute] = $value; |
|
325 | + |
|
326 | + return $new; |
|
327 | + } |
|
328 | + |
|
329 | + public function withoutAttribute($attribute): ServerRequestInterface |
|
330 | + { |
|
331 | + if (false === array_key_exists($attribute, $this->attributes)) { |
|
332 | + return $this; |
|
333 | + } |
|
334 | + |
|
335 | + $new = clone $this; |
|
336 | + unset($new->attributes[$attribute]); |
|
337 | + |
|
338 | + return $new; |
|
339 | + } |
|
340 | 340 | } |
@@ -122,8 +122,8 @@ |
||
122 | 122 | |
123 | 123 | return new UploadedFile( |
124 | 124 | $value['tmp_name'], |
125 | - (int) $value['size'], |
|
126 | - (int) $value['error'], |
|
125 | + (int)$value['size'], |
|
126 | + (int)$value['error'], |
|
127 | 127 | $value['name'], |
128 | 128 | $value['type'] |
129 | 129 | ); |
@@ -24,8 +24,7 @@ |
||
24 | 24 | * implemented such that they retain the internal state of the current |
25 | 25 | * message and return a new instance that contains the changed state. |
26 | 26 | */ |
27 | -class ServerRequest extends Request implements ServerRequestInterface |
|
28 | -{ |
|
27 | +class ServerRequest extends Request implements ServerRequestInterface { |
|
29 | 28 | /** |
30 | 29 | * @var array |
31 | 30 | */ |
@@ -12,154 +12,154 @@ |
||
12 | 12 | */ |
13 | 13 | final class MultipartStream implements StreamInterface |
14 | 14 | { |
15 | - use StreamDecoratorTrait; |
|
16 | - |
|
17 | - /** @var string */ |
|
18 | - private $boundary; |
|
19 | - |
|
20 | - /** @var StreamInterface */ |
|
21 | - private $stream; |
|
22 | - |
|
23 | - /** |
|
24 | - * @param array $elements Array of associative arrays, each containing a |
|
25 | - * required "name" key mapping to the form field, |
|
26 | - * name, a required "contents" key mapping to a |
|
27 | - * StreamInterface/resource/string, an optional |
|
28 | - * "headers" associative array of custom headers, |
|
29 | - * and an optional "filename" key mapping to a |
|
30 | - * string to send as the filename in the part. |
|
31 | - * @param string $boundary You can optionally provide a specific boundary |
|
32 | - * |
|
33 | - * @throws \InvalidArgumentException |
|
34 | - */ |
|
35 | - public function __construct(array $elements = [], string $boundary = null) |
|
36 | - { |
|
37 | - $this->boundary = $boundary ?: bin2hex(random_bytes(20)); |
|
38 | - $this->stream = $this->createStream($elements); |
|
39 | - } |
|
40 | - |
|
41 | - public function getBoundary(): string |
|
42 | - { |
|
43 | - return $this->boundary; |
|
44 | - } |
|
45 | - |
|
46 | - public function isWritable(): bool |
|
47 | - { |
|
48 | - return false; |
|
49 | - } |
|
50 | - |
|
51 | - /** |
|
52 | - * Get the headers needed before transferring the content of a POST file |
|
53 | - * |
|
54 | - * @param string[] $headers |
|
55 | - */ |
|
56 | - private function getHeaders(array $headers): string |
|
57 | - { |
|
58 | - $str = ''; |
|
59 | - foreach ($headers as $key => $value) { |
|
60 | - $str .= "{$key}: {$value}\r\n"; |
|
61 | - } |
|
62 | - |
|
63 | - return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n"; |
|
64 | - } |
|
65 | - |
|
66 | - /** |
|
67 | - * Create the aggregate stream that will be used to upload the POST data |
|
68 | - */ |
|
69 | - protected function createStream(array $elements = []): StreamInterface |
|
70 | - { |
|
71 | - $stream = new AppendStream(); |
|
72 | - |
|
73 | - foreach ($elements as $element) { |
|
74 | - if (!is_array($element)) { |
|
75 | - throw new \UnexpectedValueException('An array is expected'); |
|
76 | - } |
|
77 | - $this->addElement($stream, $element); |
|
78 | - } |
|
79 | - |
|
80 | - // Add the trailing boundary with CRLF |
|
81 | - $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n")); |
|
82 | - |
|
83 | - return $stream; |
|
84 | - } |
|
85 | - |
|
86 | - private function addElement(AppendStream $stream, array $element): void |
|
87 | - { |
|
88 | - foreach (['contents', 'name'] as $key) { |
|
89 | - if (!array_key_exists($key, $element)) { |
|
90 | - throw new \InvalidArgumentException("A '{$key}' key is required"); |
|
91 | - } |
|
92 | - } |
|
93 | - |
|
94 | - $element['contents'] = Utils::streamFor($element['contents']); |
|
95 | - |
|
96 | - if (empty($element['filename'])) { |
|
97 | - $uri = $element['contents']->getMetadata('uri'); |
|
98 | - if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { |
|
99 | - $element['filename'] = $uri; |
|
100 | - } |
|
101 | - } |
|
102 | - |
|
103 | - [$body, $headers] = $this->createElement( |
|
104 | - $element['name'], |
|
105 | - $element['contents'], |
|
106 | - $element['filename'] ?? null, |
|
107 | - $element['headers'] ?? [] |
|
108 | - ); |
|
109 | - |
|
110 | - $stream->addStream(Utils::streamFor($this->getHeaders($headers))); |
|
111 | - $stream->addStream($body); |
|
112 | - $stream->addStream(Utils::streamFor("\r\n")); |
|
113 | - } |
|
114 | - |
|
115 | - /** |
|
116 | - * @param string[] $headers |
|
117 | - * |
|
118 | - * @return array{0: StreamInterface, 1: string[]} |
|
119 | - */ |
|
120 | - private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array |
|
121 | - { |
|
122 | - // Set a default content-disposition header if one was no provided |
|
123 | - $disposition = self::getHeader($headers, 'content-disposition'); |
|
124 | - if (!$disposition) { |
|
125 | - $headers['Content-Disposition'] = ($filename === '0' || $filename) |
|
126 | - ? sprintf( |
|
127 | - 'form-data; name="%s"; filename="%s"', |
|
128 | - $name, |
|
129 | - basename($filename) |
|
130 | - ) |
|
131 | - : "form-data; name=\"{$name}\""; |
|
132 | - } |
|
133 | - |
|
134 | - // Set a default content-length header if one was no provided |
|
135 | - $length = self::getHeader($headers, 'content-length'); |
|
136 | - if (!$length) { |
|
137 | - if ($length = $stream->getSize()) { |
|
138 | - $headers['Content-Length'] = (string) $length; |
|
139 | - } |
|
140 | - } |
|
141 | - |
|
142 | - // Set a default Content-Type if one was not supplied |
|
143 | - $type = self::getHeader($headers, 'content-type'); |
|
144 | - if (!$type && ($filename === '0' || $filename)) { |
|
145 | - $headers['Content-Type'] = MimeType::fromFilename($filename) ?? 'application/octet-stream'; |
|
146 | - } |
|
147 | - |
|
148 | - return [$stream, $headers]; |
|
149 | - } |
|
150 | - |
|
151 | - /** |
|
152 | - * @param string[] $headers |
|
153 | - */ |
|
154 | - private static function getHeader(array $headers, string $key): ?string |
|
155 | - { |
|
156 | - $lowercaseHeader = strtolower($key); |
|
157 | - foreach ($headers as $k => $v) { |
|
158 | - if (strtolower((string) $k) === $lowercaseHeader) { |
|
159 | - return $v; |
|
160 | - } |
|
161 | - } |
|
162 | - |
|
163 | - return null; |
|
164 | - } |
|
15 | + use StreamDecoratorTrait; |
|
16 | + |
|
17 | + /** @var string */ |
|
18 | + private $boundary; |
|
19 | + |
|
20 | + /** @var StreamInterface */ |
|
21 | + private $stream; |
|
22 | + |
|
23 | + /** |
|
24 | + * @param array $elements Array of associative arrays, each containing a |
|
25 | + * required "name" key mapping to the form field, |
|
26 | + * name, a required "contents" key mapping to a |
|
27 | + * StreamInterface/resource/string, an optional |
|
28 | + * "headers" associative array of custom headers, |
|
29 | + * and an optional "filename" key mapping to a |
|
30 | + * string to send as the filename in the part. |
|
31 | + * @param string $boundary You can optionally provide a specific boundary |
|
32 | + * |
|
33 | + * @throws \InvalidArgumentException |
|
34 | + */ |
|
35 | + public function __construct(array $elements = [], string $boundary = null) |
|
36 | + { |
|
37 | + $this->boundary = $boundary ?: bin2hex(random_bytes(20)); |
|
38 | + $this->stream = $this->createStream($elements); |
|
39 | + } |
|
40 | + |
|
41 | + public function getBoundary(): string |
|
42 | + { |
|
43 | + return $this->boundary; |
|
44 | + } |
|
45 | + |
|
46 | + public function isWritable(): bool |
|
47 | + { |
|
48 | + return false; |
|
49 | + } |
|
50 | + |
|
51 | + /** |
|
52 | + * Get the headers needed before transferring the content of a POST file |
|
53 | + * |
|
54 | + * @param string[] $headers |
|
55 | + */ |
|
56 | + private function getHeaders(array $headers): string |
|
57 | + { |
|
58 | + $str = ''; |
|
59 | + foreach ($headers as $key => $value) { |
|
60 | + $str .= "{$key}: {$value}\r\n"; |
|
61 | + } |
|
62 | + |
|
63 | + return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n"; |
|
64 | + } |
|
65 | + |
|
66 | + /** |
|
67 | + * Create the aggregate stream that will be used to upload the POST data |
|
68 | + */ |
|
69 | + protected function createStream(array $elements = []): StreamInterface |
|
70 | + { |
|
71 | + $stream = new AppendStream(); |
|
72 | + |
|
73 | + foreach ($elements as $element) { |
|
74 | + if (!is_array($element)) { |
|
75 | + throw new \UnexpectedValueException('An array is expected'); |
|
76 | + } |
|
77 | + $this->addElement($stream, $element); |
|
78 | + } |
|
79 | + |
|
80 | + // Add the trailing boundary with CRLF |
|
81 | + $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n")); |
|
82 | + |
|
83 | + return $stream; |
|
84 | + } |
|
85 | + |
|
86 | + private function addElement(AppendStream $stream, array $element): void |
|
87 | + { |
|
88 | + foreach (['contents', 'name'] as $key) { |
|
89 | + if (!array_key_exists($key, $element)) { |
|
90 | + throw new \InvalidArgumentException("A '{$key}' key is required"); |
|
91 | + } |
|
92 | + } |
|
93 | + |
|
94 | + $element['contents'] = Utils::streamFor($element['contents']); |
|
95 | + |
|
96 | + if (empty($element['filename'])) { |
|
97 | + $uri = $element['contents']->getMetadata('uri'); |
|
98 | + if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { |
|
99 | + $element['filename'] = $uri; |
|
100 | + } |
|
101 | + } |
|
102 | + |
|
103 | + [$body, $headers] = $this->createElement( |
|
104 | + $element['name'], |
|
105 | + $element['contents'], |
|
106 | + $element['filename'] ?? null, |
|
107 | + $element['headers'] ?? [] |
|
108 | + ); |
|
109 | + |
|
110 | + $stream->addStream(Utils::streamFor($this->getHeaders($headers))); |
|
111 | + $stream->addStream($body); |
|
112 | + $stream->addStream(Utils::streamFor("\r\n")); |
|
113 | + } |
|
114 | + |
|
115 | + /** |
|
116 | + * @param string[] $headers |
|
117 | + * |
|
118 | + * @return array{0: StreamInterface, 1: string[]} |
|
119 | + */ |
|
120 | + private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array |
|
121 | + { |
|
122 | + // Set a default content-disposition header if one was no provided |
|
123 | + $disposition = self::getHeader($headers, 'content-disposition'); |
|
124 | + if (!$disposition) { |
|
125 | + $headers['Content-Disposition'] = ($filename === '0' || $filename) |
|
126 | + ? sprintf( |
|
127 | + 'form-data; name="%s"; filename="%s"', |
|
128 | + $name, |
|
129 | + basename($filename) |
|
130 | + ) |
|
131 | + : "form-data; name=\"{$name}\""; |
|
132 | + } |
|
133 | + |
|
134 | + // Set a default content-length header if one was no provided |
|
135 | + $length = self::getHeader($headers, 'content-length'); |
|
136 | + if (!$length) { |
|
137 | + if ($length = $stream->getSize()) { |
|
138 | + $headers['Content-Length'] = (string) $length; |
|
139 | + } |
|
140 | + } |
|
141 | + |
|
142 | + // Set a default Content-Type if one was not supplied |
|
143 | + $type = self::getHeader($headers, 'content-type'); |
|
144 | + if (!$type && ($filename === '0' || $filename)) { |
|
145 | + $headers['Content-Type'] = MimeType::fromFilename($filename) ?? 'application/octet-stream'; |
|
146 | + } |
|
147 | + |
|
148 | + return [$stream, $headers]; |
|
149 | + } |
|
150 | + |
|
151 | + /** |
|
152 | + * @param string[] $headers |
|
153 | + */ |
|
154 | + private static function getHeader(array $headers, string $key): ?string |
|
155 | + { |
|
156 | + $lowercaseHeader = strtolower($key); |
|
157 | + foreach ($headers as $k => $v) { |
|
158 | + if (strtolower((string) $k) === $lowercaseHeader) { |
|
159 | + return $v; |
|
160 | + } |
|
161 | + } |
|
162 | + |
|
163 | + return null; |
|
164 | + } |
|
165 | 165 | } |
@@ -135,7 +135,7 @@ discard block |
||
135 | 135 | $length = self::getHeader($headers, 'content-length'); |
136 | 136 | if (!$length) { |
137 | 137 | if ($length = $stream->getSize()) { |
138 | - $headers['Content-Length'] = (string) $length; |
|
138 | + $headers['Content-Length'] = (string)$length; |
|
139 | 139 | } |
140 | 140 | } |
141 | 141 | |
@@ -155,7 +155,7 @@ discard block |
||
155 | 155 | { |
156 | 156 | $lowercaseHeader = strtolower($key); |
157 | 157 | foreach ($headers as $k => $v) { |
158 | - if (strtolower((string) $k) === $lowercaseHeader) { |
|
158 | + if (strtolower((string)$k) === $lowercaseHeader) { |
|
159 | 159 | return $v; |
160 | 160 | } |
161 | 161 | } |
@@ -10,8 +10,7 @@ |
||
10 | 10 | * Stream that when read returns bytes for a streaming multipart or |
11 | 11 | * multipart/form-data stream. |
12 | 12 | */ |
13 | -final class MultipartStream implements StreamInterface |
|
14 | -{ |
|
13 | +final class MultipartStream implements StreamInterface { |
|
15 | 14 | use StreamDecoratorTrait; |
16 | 15 | |
17 | 16 | /** @var string */ |
@@ -12,38 +12,38 @@ |
||
12 | 12 | */ |
13 | 13 | final class LazyOpenStream implements StreamInterface |
14 | 14 | { |
15 | - use StreamDecoratorTrait; |
|
16 | - |
|
17 | - /** @var string */ |
|
18 | - private $filename; |
|
19 | - |
|
20 | - /** @var string */ |
|
21 | - private $mode; |
|
22 | - |
|
23 | - /** |
|
24 | - * @var StreamInterface |
|
25 | - */ |
|
26 | - private $stream; |
|
27 | - |
|
28 | - /** |
|
29 | - * @param string $filename File to lazily open |
|
30 | - * @param string $mode fopen mode to use when opening the stream |
|
31 | - */ |
|
32 | - public function __construct(string $filename, string $mode) |
|
33 | - { |
|
34 | - $this->filename = $filename; |
|
35 | - $this->mode = $mode; |
|
36 | - |
|
37 | - // unsetting the property forces the first access to go through |
|
38 | - // __get(). |
|
39 | - unset($this->stream); |
|
40 | - } |
|
41 | - |
|
42 | - /** |
|
43 | - * Creates the underlying stream lazily when required. |
|
44 | - */ |
|
45 | - protected function createStream(): StreamInterface |
|
46 | - { |
|
47 | - return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); |
|
48 | - } |
|
15 | + use StreamDecoratorTrait; |
|
16 | + |
|
17 | + /** @var string */ |
|
18 | + private $filename; |
|
19 | + |
|
20 | + /** @var string */ |
|
21 | + private $mode; |
|
22 | + |
|
23 | + /** |
|
24 | + * @var StreamInterface |
|
25 | + */ |
|
26 | + private $stream; |
|
27 | + |
|
28 | + /** |
|
29 | + * @param string $filename File to lazily open |
|
30 | + * @param string $mode fopen mode to use when opening the stream |
|
31 | + */ |
|
32 | + public function __construct(string $filename, string $mode) |
|
33 | + { |
|
34 | + $this->filename = $filename; |
|
35 | + $this->mode = $mode; |
|
36 | + |
|
37 | + // unsetting the property forces the first access to go through |
|
38 | + // __get(). |
|
39 | + unset($this->stream); |
|
40 | + } |
|
41 | + |
|
42 | + /** |
|
43 | + * Creates the underlying stream lazily when required. |
|
44 | + */ |
|
45 | + protected function createStream(): StreamInterface |
|
46 | + { |
|
47 | + return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); |
|
48 | + } |
|
49 | 49 | } |
@@ -10,8 +10,7 @@ |
||
10 | 10 | * Lazily reads or writes to a file that is opened only after an IO operation |
11 | 11 | * take place on the stream. |
12 | 12 | */ |
13 | -final class LazyOpenStream implements StreamInterface |
|
14 | -{ |
|
13 | +final class LazyOpenStream implements StreamInterface { |
|
15 | 14 | use StreamDecoratorTrait; |
16 | 15 | |
17 | 16 | /** @var string */ |
@@ -19,19 +19,19 @@ |
||
19 | 19 | */ |
20 | 20 | final class InflateStream implements StreamInterface |
21 | 21 | { |
22 | - use StreamDecoratorTrait; |
|
22 | + use StreamDecoratorTrait; |
|
23 | 23 | |
24 | - /** @var StreamInterface */ |
|
25 | - private $stream; |
|
24 | + /** @var StreamInterface */ |
|
25 | + private $stream; |
|
26 | 26 | |
27 | - public function __construct(StreamInterface $stream) |
|
28 | - { |
|
29 | - $resource = StreamWrapper::getResource($stream); |
|
30 | - // Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data |
|
31 | - // See https://www.zlib.net/manual.html#Advanced definition of inflateInit2 |
|
32 | - // "Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" |
|
33 | - // Default window size is 15. |
|
34 | - stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15 + 32]); |
|
35 | - $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); |
|
36 | - } |
|
27 | + public function __construct(StreamInterface $stream) |
|
28 | + { |
|
29 | + $resource = StreamWrapper::getResource($stream); |
|
30 | + // Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data |
|
31 | + // See https://www.zlib.net/manual.html#Advanced definition of inflateInit2 |
|
32 | + // "Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" |
|
33 | + // Default window size is 15. |
|
34 | + stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15 + 32]); |
|
35 | + $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); |
|
36 | + } |
|
37 | 37 | } |
@@ -17,8 +17,7 @@ |
||
17 | 17 | * @see https://datatracker.ietf.org/doc/html/rfc1952 |
18 | 18 | * @see https://www.php.net/manual/en/filters.compression.php |
19 | 19 | */ |
20 | -final class InflateStream implements StreamInterface |
|
21 | -{ |
|
20 | +final class InflateStream implements StreamInterface { |
|
22 | 21 | use StreamDecoratorTrait; |
23 | 22 | |
24 | 23 | /** @var StreamInterface */ |
@@ -11,273 +11,273 @@ |
||
11 | 11 | */ |
12 | 12 | class Stream implements StreamInterface |
13 | 13 | { |
14 | - /** |
|
15 | - * @see https://www.php.net/manual/en/function.fopen.php |
|
16 | - * @see https://www.php.net/manual/en/function.gzopen.php |
|
17 | - */ |
|
18 | - private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/'; |
|
19 | - private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; |
|
20 | - |
|
21 | - /** @var resource */ |
|
22 | - private $stream; |
|
23 | - /** @var int|null */ |
|
24 | - private $size; |
|
25 | - /** @var bool */ |
|
26 | - private $seekable; |
|
27 | - /** @var bool */ |
|
28 | - private $readable; |
|
29 | - /** @var bool */ |
|
30 | - private $writable; |
|
31 | - /** @var string|null */ |
|
32 | - private $uri; |
|
33 | - /** @var mixed[] */ |
|
34 | - private $customMetadata; |
|
35 | - |
|
36 | - /** |
|
37 | - * This constructor accepts an associative array of options. |
|
38 | - * |
|
39 | - * - size: (int) If a read stream would otherwise have an indeterminate |
|
40 | - * size, but the size is known due to foreknowledge, then you can |
|
41 | - * provide that size, in bytes. |
|
42 | - * - metadata: (array) Any additional metadata to return when the metadata |
|
43 | - * of the stream is accessed. |
|
44 | - * |
|
45 | - * @param resource $stream Stream resource to wrap. |
|
46 | - * @param array{size?: int, metadata?: array} $options Associative array of options. |
|
47 | - * |
|
48 | - * @throws \InvalidArgumentException if the stream is not a stream resource |
|
49 | - */ |
|
50 | - public function __construct($stream, array $options = []) |
|
51 | - { |
|
52 | - if (!is_resource($stream)) { |
|
53 | - throw new \InvalidArgumentException('Stream must be a resource'); |
|
54 | - } |
|
55 | - |
|
56 | - if (isset($options['size'])) { |
|
57 | - $this->size = $options['size']; |
|
58 | - } |
|
59 | - |
|
60 | - $this->customMetadata = $options['metadata'] ?? []; |
|
61 | - $this->stream = $stream; |
|
62 | - $meta = stream_get_meta_data($this->stream); |
|
63 | - $this->seekable = $meta['seekable']; |
|
64 | - $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); |
|
65 | - $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); |
|
66 | - $this->uri = $this->getMetadata('uri'); |
|
67 | - } |
|
68 | - |
|
69 | - /** |
|
70 | - * Closes the stream when the destructed |
|
71 | - */ |
|
72 | - public function __destruct() |
|
73 | - { |
|
74 | - $this->close(); |
|
75 | - } |
|
76 | - |
|
77 | - public function __toString(): string |
|
78 | - { |
|
79 | - try { |
|
80 | - if ($this->isSeekable()) { |
|
81 | - $this->seek(0); |
|
82 | - } |
|
83 | - |
|
84 | - return $this->getContents(); |
|
85 | - } catch (\Throwable $e) { |
|
86 | - if (\PHP_VERSION_ID >= 70400) { |
|
87 | - throw $e; |
|
88 | - } |
|
89 | - trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
90 | - |
|
91 | - return ''; |
|
92 | - } |
|
93 | - } |
|
94 | - |
|
95 | - public function getContents(): string |
|
96 | - { |
|
97 | - if (!isset($this->stream)) { |
|
98 | - throw new \RuntimeException('Stream is detached'); |
|
99 | - } |
|
100 | - |
|
101 | - if (!$this->readable) { |
|
102 | - throw new \RuntimeException('Cannot read from non-readable stream'); |
|
103 | - } |
|
104 | - |
|
105 | - return Utils::tryGetContents($this->stream); |
|
106 | - } |
|
107 | - |
|
108 | - public function close(): void |
|
109 | - { |
|
110 | - if (isset($this->stream)) { |
|
111 | - if (is_resource($this->stream)) { |
|
112 | - fclose($this->stream); |
|
113 | - } |
|
114 | - $this->detach(); |
|
115 | - } |
|
116 | - } |
|
117 | - |
|
118 | - public function detach() |
|
119 | - { |
|
120 | - if (!isset($this->stream)) { |
|
121 | - return null; |
|
122 | - } |
|
123 | - |
|
124 | - $result = $this->stream; |
|
125 | - unset($this->stream); |
|
126 | - $this->size = $this->uri = null; |
|
127 | - $this->readable = $this->writable = $this->seekable = false; |
|
128 | - |
|
129 | - return $result; |
|
130 | - } |
|
131 | - |
|
132 | - public function getSize(): ?int |
|
133 | - { |
|
134 | - if ($this->size !== null) { |
|
135 | - return $this->size; |
|
136 | - } |
|
137 | - |
|
138 | - if (!isset($this->stream)) { |
|
139 | - return null; |
|
140 | - } |
|
141 | - |
|
142 | - // Clear the stat cache if the stream has a URI |
|
143 | - if ($this->uri) { |
|
144 | - clearstatcache(true, $this->uri); |
|
145 | - } |
|
146 | - |
|
147 | - $stats = fstat($this->stream); |
|
148 | - if (is_array($stats) && isset($stats['size'])) { |
|
149 | - $this->size = $stats['size']; |
|
150 | - |
|
151 | - return $this->size; |
|
152 | - } |
|
153 | - |
|
154 | - return null; |
|
155 | - } |
|
156 | - |
|
157 | - public function isReadable(): bool |
|
158 | - { |
|
159 | - return $this->readable; |
|
160 | - } |
|
161 | - |
|
162 | - public function isWritable(): bool |
|
163 | - { |
|
164 | - return $this->writable; |
|
165 | - } |
|
166 | - |
|
167 | - public function isSeekable(): bool |
|
168 | - { |
|
169 | - return $this->seekable; |
|
170 | - } |
|
171 | - |
|
172 | - public function eof(): bool |
|
173 | - { |
|
174 | - if (!isset($this->stream)) { |
|
175 | - throw new \RuntimeException('Stream is detached'); |
|
176 | - } |
|
177 | - |
|
178 | - return feof($this->stream); |
|
179 | - } |
|
180 | - |
|
181 | - public function tell(): int |
|
182 | - { |
|
183 | - if (!isset($this->stream)) { |
|
184 | - throw new \RuntimeException('Stream is detached'); |
|
185 | - } |
|
186 | - |
|
187 | - $result = ftell($this->stream); |
|
188 | - |
|
189 | - if ($result === false) { |
|
190 | - throw new \RuntimeException('Unable to determine stream position'); |
|
191 | - } |
|
192 | - |
|
193 | - return $result; |
|
194 | - } |
|
195 | - |
|
196 | - public function rewind(): void |
|
197 | - { |
|
198 | - $this->seek(0); |
|
199 | - } |
|
200 | - |
|
201 | - public function seek($offset, $whence = SEEK_SET): void |
|
202 | - { |
|
203 | - $whence = (int) $whence; |
|
204 | - |
|
205 | - if (!isset($this->stream)) { |
|
206 | - throw new \RuntimeException('Stream is detached'); |
|
207 | - } |
|
208 | - if (!$this->seekable) { |
|
209 | - throw new \RuntimeException('Stream is not seekable'); |
|
210 | - } |
|
211 | - if (fseek($this->stream, $offset, $whence) === -1) { |
|
212 | - throw new \RuntimeException('Unable to seek to stream position ' |
|
213 | - .$offset.' with whence '.var_export($whence, true)); |
|
214 | - } |
|
215 | - } |
|
216 | - |
|
217 | - public function read($length): string |
|
218 | - { |
|
219 | - if (!isset($this->stream)) { |
|
220 | - throw new \RuntimeException('Stream is detached'); |
|
221 | - } |
|
222 | - if (!$this->readable) { |
|
223 | - throw new \RuntimeException('Cannot read from non-readable stream'); |
|
224 | - } |
|
225 | - if ($length < 0) { |
|
226 | - throw new \RuntimeException('Length parameter cannot be negative'); |
|
227 | - } |
|
228 | - |
|
229 | - if (0 === $length) { |
|
230 | - return ''; |
|
231 | - } |
|
232 | - |
|
233 | - try { |
|
234 | - $string = fread($this->stream, $length); |
|
235 | - } catch (\Exception $e) { |
|
236 | - throw new \RuntimeException('Unable to read from stream', 0, $e); |
|
237 | - } |
|
238 | - |
|
239 | - if (false === $string) { |
|
240 | - throw new \RuntimeException('Unable to read from stream'); |
|
241 | - } |
|
242 | - |
|
243 | - return $string; |
|
244 | - } |
|
245 | - |
|
246 | - public function write($string): int |
|
247 | - { |
|
248 | - if (!isset($this->stream)) { |
|
249 | - throw new \RuntimeException('Stream is detached'); |
|
250 | - } |
|
251 | - if (!$this->writable) { |
|
252 | - throw new \RuntimeException('Cannot write to a non-writable stream'); |
|
253 | - } |
|
254 | - |
|
255 | - // We can't know the size after writing anything |
|
256 | - $this->size = null; |
|
257 | - $result = fwrite($this->stream, $string); |
|
258 | - |
|
259 | - if ($result === false) { |
|
260 | - throw new \RuntimeException('Unable to write to stream'); |
|
261 | - } |
|
262 | - |
|
263 | - return $result; |
|
264 | - } |
|
265 | - |
|
266 | - /** |
|
267 | - * @return mixed |
|
268 | - */ |
|
269 | - public function getMetadata($key = null) |
|
270 | - { |
|
271 | - if (!isset($this->stream)) { |
|
272 | - return $key ? null : []; |
|
273 | - } elseif (!$key) { |
|
274 | - return $this->customMetadata + stream_get_meta_data($this->stream); |
|
275 | - } elseif (isset($this->customMetadata[$key])) { |
|
276 | - return $this->customMetadata[$key]; |
|
277 | - } |
|
278 | - |
|
279 | - $meta = stream_get_meta_data($this->stream); |
|
280 | - |
|
281 | - return $meta[$key] ?? null; |
|
282 | - } |
|
14 | + /** |
|
15 | + * @see https://www.php.net/manual/en/function.fopen.php |
|
16 | + * @see https://www.php.net/manual/en/function.gzopen.php |
|
17 | + */ |
|
18 | + private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/'; |
|
19 | + private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; |
|
20 | + |
|
21 | + /** @var resource */ |
|
22 | + private $stream; |
|
23 | + /** @var int|null */ |
|
24 | + private $size; |
|
25 | + /** @var bool */ |
|
26 | + private $seekable; |
|
27 | + /** @var bool */ |
|
28 | + private $readable; |
|
29 | + /** @var bool */ |
|
30 | + private $writable; |
|
31 | + /** @var string|null */ |
|
32 | + private $uri; |
|
33 | + /** @var mixed[] */ |
|
34 | + private $customMetadata; |
|
35 | + |
|
36 | + /** |
|
37 | + * This constructor accepts an associative array of options. |
|
38 | + * |
|
39 | + * - size: (int) If a read stream would otherwise have an indeterminate |
|
40 | + * size, but the size is known due to foreknowledge, then you can |
|
41 | + * provide that size, in bytes. |
|
42 | + * - metadata: (array) Any additional metadata to return when the metadata |
|
43 | + * of the stream is accessed. |
|
44 | + * |
|
45 | + * @param resource $stream Stream resource to wrap. |
|
46 | + * @param array{size?: int, metadata?: array} $options Associative array of options. |
|
47 | + * |
|
48 | + * @throws \InvalidArgumentException if the stream is not a stream resource |
|
49 | + */ |
|
50 | + public function __construct($stream, array $options = []) |
|
51 | + { |
|
52 | + if (!is_resource($stream)) { |
|
53 | + throw new \InvalidArgumentException('Stream must be a resource'); |
|
54 | + } |
|
55 | + |
|
56 | + if (isset($options['size'])) { |
|
57 | + $this->size = $options['size']; |
|
58 | + } |
|
59 | + |
|
60 | + $this->customMetadata = $options['metadata'] ?? []; |
|
61 | + $this->stream = $stream; |
|
62 | + $meta = stream_get_meta_data($this->stream); |
|
63 | + $this->seekable = $meta['seekable']; |
|
64 | + $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); |
|
65 | + $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); |
|
66 | + $this->uri = $this->getMetadata('uri'); |
|
67 | + } |
|
68 | + |
|
69 | + /** |
|
70 | + * Closes the stream when the destructed |
|
71 | + */ |
|
72 | + public function __destruct() |
|
73 | + { |
|
74 | + $this->close(); |
|
75 | + } |
|
76 | + |
|
77 | + public function __toString(): string |
|
78 | + { |
|
79 | + try { |
|
80 | + if ($this->isSeekable()) { |
|
81 | + $this->seek(0); |
|
82 | + } |
|
83 | + |
|
84 | + return $this->getContents(); |
|
85 | + } catch (\Throwable $e) { |
|
86 | + if (\PHP_VERSION_ID >= 70400) { |
|
87 | + throw $e; |
|
88 | + } |
|
89 | + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
90 | + |
|
91 | + return ''; |
|
92 | + } |
|
93 | + } |
|
94 | + |
|
95 | + public function getContents(): string |
|
96 | + { |
|
97 | + if (!isset($this->stream)) { |
|
98 | + throw new \RuntimeException('Stream is detached'); |
|
99 | + } |
|
100 | + |
|
101 | + if (!$this->readable) { |
|
102 | + throw new \RuntimeException('Cannot read from non-readable stream'); |
|
103 | + } |
|
104 | + |
|
105 | + return Utils::tryGetContents($this->stream); |
|
106 | + } |
|
107 | + |
|
108 | + public function close(): void |
|
109 | + { |
|
110 | + if (isset($this->stream)) { |
|
111 | + if (is_resource($this->stream)) { |
|
112 | + fclose($this->stream); |
|
113 | + } |
|
114 | + $this->detach(); |
|
115 | + } |
|
116 | + } |
|
117 | + |
|
118 | + public function detach() |
|
119 | + { |
|
120 | + if (!isset($this->stream)) { |
|
121 | + return null; |
|
122 | + } |
|
123 | + |
|
124 | + $result = $this->stream; |
|
125 | + unset($this->stream); |
|
126 | + $this->size = $this->uri = null; |
|
127 | + $this->readable = $this->writable = $this->seekable = false; |
|
128 | + |
|
129 | + return $result; |
|
130 | + } |
|
131 | + |
|
132 | + public function getSize(): ?int |
|
133 | + { |
|
134 | + if ($this->size !== null) { |
|
135 | + return $this->size; |
|
136 | + } |
|
137 | + |
|
138 | + if (!isset($this->stream)) { |
|
139 | + return null; |
|
140 | + } |
|
141 | + |
|
142 | + // Clear the stat cache if the stream has a URI |
|
143 | + if ($this->uri) { |
|
144 | + clearstatcache(true, $this->uri); |
|
145 | + } |
|
146 | + |
|
147 | + $stats = fstat($this->stream); |
|
148 | + if (is_array($stats) && isset($stats['size'])) { |
|
149 | + $this->size = $stats['size']; |
|
150 | + |
|
151 | + return $this->size; |
|
152 | + } |
|
153 | + |
|
154 | + return null; |
|
155 | + } |
|
156 | + |
|
157 | + public function isReadable(): bool |
|
158 | + { |
|
159 | + return $this->readable; |
|
160 | + } |
|
161 | + |
|
162 | + public function isWritable(): bool |
|
163 | + { |
|
164 | + return $this->writable; |
|
165 | + } |
|
166 | + |
|
167 | + public function isSeekable(): bool |
|
168 | + { |
|
169 | + return $this->seekable; |
|
170 | + } |
|
171 | + |
|
172 | + public function eof(): bool |
|
173 | + { |
|
174 | + if (!isset($this->stream)) { |
|
175 | + throw new \RuntimeException('Stream is detached'); |
|
176 | + } |
|
177 | + |
|
178 | + return feof($this->stream); |
|
179 | + } |
|
180 | + |
|
181 | + public function tell(): int |
|
182 | + { |
|
183 | + if (!isset($this->stream)) { |
|
184 | + throw new \RuntimeException('Stream is detached'); |
|
185 | + } |
|
186 | + |
|
187 | + $result = ftell($this->stream); |
|
188 | + |
|
189 | + if ($result === false) { |
|
190 | + throw new \RuntimeException('Unable to determine stream position'); |
|
191 | + } |
|
192 | + |
|
193 | + return $result; |
|
194 | + } |
|
195 | + |
|
196 | + public function rewind(): void |
|
197 | + { |
|
198 | + $this->seek(0); |
|
199 | + } |
|
200 | + |
|
201 | + public function seek($offset, $whence = SEEK_SET): void |
|
202 | + { |
|
203 | + $whence = (int) $whence; |
|
204 | + |
|
205 | + if (!isset($this->stream)) { |
|
206 | + throw new \RuntimeException('Stream is detached'); |
|
207 | + } |
|
208 | + if (!$this->seekable) { |
|
209 | + throw new \RuntimeException('Stream is not seekable'); |
|
210 | + } |
|
211 | + if (fseek($this->stream, $offset, $whence) === -1) { |
|
212 | + throw new \RuntimeException('Unable to seek to stream position ' |
|
213 | + .$offset.' with whence '.var_export($whence, true)); |
|
214 | + } |
|
215 | + } |
|
216 | + |
|
217 | + public function read($length): string |
|
218 | + { |
|
219 | + if (!isset($this->stream)) { |
|
220 | + throw new \RuntimeException('Stream is detached'); |
|
221 | + } |
|
222 | + if (!$this->readable) { |
|
223 | + throw new \RuntimeException('Cannot read from non-readable stream'); |
|
224 | + } |
|
225 | + if ($length < 0) { |
|
226 | + throw new \RuntimeException('Length parameter cannot be negative'); |
|
227 | + } |
|
228 | + |
|
229 | + if (0 === $length) { |
|
230 | + return ''; |
|
231 | + } |
|
232 | + |
|
233 | + try { |
|
234 | + $string = fread($this->stream, $length); |
|
235 | + } catch (\Exception $e) { |
|
236 | + throw new \RuntimeException('Unable to read from stream', 0, $e); |
|
237 | + } |
|
238 | + |
|
239 | + if (false === $string) { |
|
240 | + throw new \RuntimeException('Unable to read from stream'); |
|
241 | + } |
|
242 | + |
|
243 | + return $string; |
|
244 | + } |
|
245 | + |
|
246 | + public function write($string): int |
|
247 | + { |
|
248 | + if (!isset($this->stream)) { |
|
249 | + throw new \RuntimeException('Stream is detached'); |
|
250 | + } |
|
251 | + if (!$this->writable) { |
|
252 | + throw new \RuntimeException('Cannot write to a non-writable stream'); |
|
253 | + } |
|
254 | + |
|
255 | + // We can't know the size after writing anything |
|
256 | + $this->size = null; |
|
257 | + $result = fwrite($this->stream, $string); |
|
258 | + |
|
259 | + if ($result === false) { |
|
260 | + throw new \RuntimeException('Unable to write to stream'); |
|
261 | + } |
|
262 | + |
|
263 | + return $result; |
|
264 | + } |
|
265 | + |
|
266 | + /** |
|
267 | + * @return mixed |
|
268 | + */ |
|
269 | + public function getMetadata($key = null) |
|
270 | + { |
|
271 | + if (!isset($this->stream)) { |
|
272 | + return $key ? null : []; |
|
273 | + } elseif (!$key) { |
|
274 | + return $this->customMetadata + stream_get_meta_data($this->stream); |
|
275 | + } elseif (isset($this->customMetadata[$key])) { |
|
276 | + return $this->customMetadata[$key]; |
|
277 | + } |
|
278 | + |
|
279 | + $meta = stream_get_meta_data($this->stream); |
|
280 | + |
|
281 | + return $meta[$key] ?? null; |
|
282 | + } |
|
283 | 283 | } |
@@ -61,8 +61,8 @@ discard block |
||
61 | 61 | $this->stream = $stream; |
62 | 62 | $meta = stream_get_meta_data($this->stream); |
63 | 63 | $this->seekable = $meta['seekable']; |
64 | - $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); |
|
65 | - $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); |
|
64 | + $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); |
|
65 | + $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); |
|
66 | 66 | $this->uri = $this->getMetadata('uri'); |
67 | 67 | } |
68 | 68 | |
@@ -86,7 +86,7 @@ discard block |
||
86 | 86 | if (\PHP_VERSION_ID >= 70400) { |
87 | 87 | throw $e; |
88 | 88 | } |
89 | - trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
89 | + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string)$e), E_USER_ERROR); |
|
90 | 90 | |
91 | 91 | return ''; |
92 | 92 | } |
@@ -200,7 +200,7 @@ discard block |
||
200 | 200 | |
201 | 201 | public function seek($offset, $whence = SEEK_SET): void |
202 | 202 | { |
203 | - $whence = (int) $whence; |
|
203 | + $whence = (int)$whence; |
|
204 | 204 | |
205 | 205 | if (!isset($this->stream)) { |
206 | 206 | throw new \RuntimeException('Stream is detached'); |
@@ -9,8 +9,7 @@ |
||
9 | 9 | /** |
10 | 10 | * PHP stream implementation. |
11 | 11 | */ |
12 | -class Stream implements StreamInterface |
|
13 | -{ |
|
12 | +class Stream implements StreamInterface { |
|
14 | 13 | /** |
15 | 14 | * @see https://www.php.net/manual/en/function.fopen.php |
16 | 15 | * @see https://www.php.net/manual/en/function.gzopen.php |
@@ -10,237 +10,237 @@ |
||
10 | 10 | |
11 | 11 | final class Message |
12 | 12 | { |
13 | - /** |
|
14 | - * Returns the string representation of an HTTP message. |
|
15 | - * |
|
16 | - * @param MessageInterface $message Message to convert to a string. |
|
17 | - */ |
|
18 | - public static function toString(MessageInterface $message): string |
|
19 | - { |
|
20 | - if ($message instanceof RequestInterface) { |
|
21 | - $msg = trim($message->getMethod().' ' |
|
22 | - .$message->getRequestTarget()) |
|
23 | - .' HTTP/'.$message->getProtocolVersion(); |
|
24 | - if (!$message->hasHeader('host')) { |
|
25 | - $msg .= "\r\nHost: ".$message->getUri()->getHost(); |
|
26 | - } |
|
27 | - } elseif ($message instanceof ResponseInterface) { |
|
28 | - $msg = 'HTTP/'.$message->getProtocolVersion().' ' |
|
29 | - .$message->getStatusCode().' ' |
|
30 | - .$message->getReasonPhrase(); |
|
31 | - } else { |
|
32 | - throw new \InvalidArgumentException('Unknown message type'); |
|
33 | - } |
|
34 | - |
|
35 | - foreach ($message->getHeaders() as $name => $values) { |
|
36 | - if (is_string($name) && strtolower($name) === 'set-cookie') { |
|
37 | - foreach ($values as $value) { |
|
38 | - $msg .= "\r\n{$name}: ".$value; |
|
39 | - } |
|
40 | - } else { |
|
41 | - $msg .= "\r\n{$name}: ".implode(', ', $values); |
|
42 | - } |
|
43 | - } |
|
44 | - |
|
45 | - return "{$msg}\r\n\r\n".$message->getBody(); |
|
46 | - } |
|
47 | - |
|
48 | - /** |
|
49 | - * Get a short summary of the message body. |
|
50 | - * |
|
51 | - * Will return `null` if the response is not printable. |
|
52 | - * |
|
53 | - * @param MessageInterface $message The message to get the body summary |
|
54 | - * @param int $truncateAt The maximum allowed size of the summary |
|
55 | - */ |
|
56 | - public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string |
|
57 | - { |
|
58 | - $body = $message->getBody(); |
|
59 | - |
|
60 | - if (!$body->isSeekable() || !$body->isReadable()) { |
|
61 | - return null; |
|
62 | - } |
|
63 | - |
|
64 | - $size = $body->getSize(); |
|
65 | - |
|
66 | - if ($size === 0) { |
|
67 | - return null; |
|
68 | - } |
|
69 | - |
|
70 | - $body->rewind(); |
|
71 | - $summary = $body->read($truncateAt); |
|
72 | - $body->rewind(); |
|
73 | - |
|
74 | - if ($size > $truncateAt) { |
|
75 | - $summary .= ' (truncated...)'; |
|
76 | - } |
|
77 | - |
|
78 | - // Matches any printable character, including unicode characters: |
|
79 | - // letters, marks, numbers, punctuation, spacing, and separators. |
|
80 | - if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) { |
|
81 | - return null; |
|
82 | - } |
|
83 | - |
|
84 | - return $summary; |
|
85 | - } |
|
86 | - |
|
87 | - /** |
|
88 | - * Attempts to rewind a message body and throws an exception on failure. |
|
89 | - * |
|
90 | - * The body of the message will only be rewound if a call to `tell()` |
|
91 | - * returns a value other than `0`. |
|
92 | - * |
|
93 | - * @param MessageInterface $message Message to rewind |
|
94 | - * |
|
95 | - * @throws \RuntimeException |
|
96 | - */ |
|
97 | - public static function rewindBody(MessageInterface $message): void |
|
98 | - { |
|
99 | - $body = $message->getBody(); |
|
100 | - |
|
101 | - if ($body->tell()) { |
|
102 | - $body->rewind(); |
|
103 | - } |
|
104 | - } |
|
105 | - |
|
106 | - /** |
|
107 | - * Parses an HTTP message into an associative array. |
|
108 | - * |
|
109 | - * The array contains the "start-line" key containing the start line of |
|
110 | - * the message, "headers" key containing an associative array of header |
|
111 | - * array values, and a "body" key containing the body of the message. |
|
112 | - * |
|
113 | - * @param string $message HTTP request or response to parse. |
|
114 | - */ |
|
115 | - public static function parseMessage(string $message): array |
|
116 | - { |
|
117 | - if (!$message) { |
|
118 | - throw new \InvalidArgumentException('Invalid message'); |
|
119 | - } |
|
120 | - |
|
121 | - $message = ltrim($message, "\r\n"); |
|
122 | - |
|
123 | - $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); |
|
124 | - |
|
125 | - if ($messageParts === false || count($messageParts) !== 2) { |
|
126 | - throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); |
|
127 | - } |
|
128 | - |
|
129 | - [$rawHeaders, $body] = $messageParts; |
|
130 | - $rawHeaders .= "\r\n"; // Put back the delimiter we split previously |
|
131 | - $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); |
|
132 | - |
|
133 | - if ($headerParts === false || count($headerParts) !== 2) { |
|
134 | - throw new \InvalidArgumentException('Invalid message: Missing status line'); |
|
135 | - } |
|
136 | - |
|
137 | - [$startLine, $rawHeaders] = $headerParts; |
|
138 | - |
|
139 | - if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { |
|
140 | - // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 |
|
141 | - $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); |
|
142 | - } |
|
143 | - |
|
144 | - /** @var array[] $headerLines */ |
|
145 | - $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); |
|
146 | - |
|
147 | - // If these aren't the same, then one line didn't match and there's an invalid header. |
|
148 | - if ($count !== substr_count($rawHeaders, "\n")) { |
|
149 | - // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 |
|
150 | - if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { |
|
151 | - throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); |
|
152 | - } |
|
153 | - |
|
154 | - throw new \InvalidArgumentException('Invalid header syntax'); |
|
155 | - } |
|
156 | - |
|
157 | - $headers = []; |
|
158 | - |
|
159 | - foreach ($headerLines as $headerLine) { |
|
160 | - $headers[$headerLine[1]][] = $headerLine[2]; |
|
161 | - } |
|
162 | - |
|
163 | - return [ |
|
164 | - 'start-line' => $startLine, |
|
165 | - 'headers' => $headers, |
|
166 | - 'body' => $body, |
|
167 | - ]; |
|
168 | - } |
|
169 | - |
|
170 | - /** |
|
171 | - * Constructs a URI for an HTTP request message. |
|
172 | - * |
|
173 | - * @param string $path Path from the start-line |
|
174 | - * @param array $headers Array of headers (each value an array). |
|
175 | - */ |
|
176 | - public static function parseRequestUri(string $path, array $headers): string |
|
177 | - { |
|
178 | - $hostKey = array_filter(array_keys($headers), function ($k) { |
|
179 | - // Numeric array keys are converted to int by PHP. |
|
180 | - $k = (string) $k; |
|
181 | - |
|
182 | - return strtolower($k) === 'host'; |
|
183 | - }); |
|
184 | - |
|
185 | - // If no host is found, then a full URI cannot be constructed. |
|
186 | - if (!$hostKey) { |
|
187 | - return $path; |
|
188 | - } |
|
189 | - |
|
190 | - $host = $headers[reset($hostKey)][0]; |
|
191 | - $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; |
|
192 | - |
|
193 | - return $scheme.'://'.$host.'/'.ltrim($path, '/'); |
|
194 | - } |
|
195 | - |
|
196 | - /** |
|
197 | - * Parses a request message string into a request object. |
|
198 | - * |
|
199 | - * @param string $message Request message string. |
|
200 | - */ |
|
201 | - public static function parseRequest(string $message): RequestInterface |
|
202 | - { |
|
203 | - $data = self::parseMessage($message); |
|
204 | - $matches = []; |
|
205 | - if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { |
|
206 | - throw new \InvalidArgumentException('Invalid request string'); |
|
207 | - } |
|
208 | - $parts = explode(' ', $data['start-line'], 3); |
|
209 | - $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; |
|
210 | - |
|
211 | - $request = new Request( |
|
212 | - $parts[0], |
|
213 | - $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], |
|
214 | - $data['headers'], |
|
215 | - $data['body'], |
|
216 | - $version |
|
217 | - ); |
|
218 | - |
|
219 | - return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); |
|
220 | - } |
|
221 | - |
|
222 | - /** |
|
223 | - * Parses a response message string into a response object. |
|
224 | - * |
|
225 | - * @param string $message Response message string. |
|
226 | - */ |
|
227 | - public static function parseResponse(string $message): ResponseInterface |
|
228 | - { |
|
229 | - $data = self::parseMessage($message); |
|
230 | - // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 |
|
231 | - // the space between status-code and reason-phrase is required. But |
|
232 | - // browsers accept responses without space and reason as well. |
|
233 | - if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { |
|
234 | - throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']); |
|
235 | - } |
|
236 | - $parts = explode(' ', $data['start-line'], 3); |
|
237 | - |
|
238 | - return new Response( |
|
239 | - (int) $parts[1], |
|
240 | - $data['headers'], |
|
241 | - $data['body'], |
|
242 | - explode('/', $parts[0])[1], |
|
243 | - $parts[2] ?? null |
|
244 | - ); |
|
245 | - } |
|
13 | + /** |
|
14 | + * Returns the string representation of an HTTP message. |
|
15 | + * |
|
16 | + * @param MessageInterface $message Message to convert to a string. |
|
17 | + */ |
|
18 | + public static function toString(MessageInterface $message): string |
|
19 | + { |
|
20 | + if ($message instanceof RequestInterface) { |
|
21 | + $msg = trim($message->getMethod().' ' |
|
22 | + .$message->getRequestTarget()) |
|
23 | + .' HTTP/'.$message->getProtocolVersion(); |
|
24 | + if (!$message->hasHeader('host')) { |
|
25 | + $msg .= "\r\nHost: ".$message->getUri()->getHost(); |
|
26 | + } |
|
27 | + } elseif ($message instanceof ResponseInterface) { |
|
28 | + $msg = 'HTTP/'.$message->getProtocolVersion().' ' |
|
29 | + .$message->getStatusCode().' ' |
|
30 | + .$message->getReasonPhrase(); |
|
31 | + } else { |
|
32 | + throw new \InvalidArgumentException('Unknown message type'); |
|
33 | + } |
|
34 | + |
|
35 | + foreach ($message->getHeaders() as $name => $values) { |
|
36 | + if (is_string($name) && strtolower($name) === 'set-cookie') { |
|
37 | + foreach ($values as $value) { |
|
38 | + $msg .= "\r\n{$name}: ".$value; |
|
39 | + } |
|
40 | + } else { |
|
41 | + $msg .= "\r\n{$name}: ".implode(', ', $values); |
|
42 | + } |
|
43 | + } |
|
44 | + |
|
45 | + return "{$msg}\r\n\r\n".$message->getBody(); |
|
46 | + } |
|
47 | + |
|
48 | + /** |
|
49 | + * Get a short summary of the message body. |
|
50 | + * |
|
51 | + * Will return `null` if the response is not printable. |
|
52 | + * |
|
53 | + * @param MessageInterface $message The message to get the body summary |
|
54 | + * @param int $truncateAt The maximum allowed size of the summary |
|
55 | + */ |
|
56 | + public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string |
|
57 | + { |
|
58 | + $body = $message->getBody(); |
|
59 | + |
|
60 | + if (!$body->isSeekable() || !$body->isReadable()) { |
|
61 | + return null; |
|
62 | + } |
|
63 | + |
|
64 | + $size = $body->getSize(); |
|
65 | + |
|
66 | + if ($size === 0) { |
|
67 | + return null; |
|
68 | + } |
|
69 | + |
|
70 | + $body->rewind(); |
|
71 | + $summary = $body->read($truncateAt); |
|
72 | + $body->rewind(); |
|
73 | + |
|
74 | + if ($size > $truncateAt) { |
|
75 | + $summary .= ' (truncated...)'; |
|
76 | + } |
|
77 | + |
|
78 | + // Matches any printable character, including unicode characters: |
|
79 | + // letters, marks, numbers, punctuation, spacing, and separators. |
|
80 | + if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) { |
|
81 | + return null; |
|
82 | + } |
|
83 | + |
|
84 | + return $summary; |
|
85 | + } |
|
86 | + |
|
87 | + /** |
|
88 | + * Attempts to rewind a message body and throws an exception on failure. |
|
89 | + * |
|
90 | + * The body of the message will only be rewound if a call to `tell()` |
|
91 | + * returns a value other than `0`. |
|
92 | + * |
|
93 | + * @param MessageInterface $message Message to rewind |
|
94 | + * |
|
95 | + * @throws \RuntimeException |
|
96 | + */ |
|
97 | + public static function rewindBody(MessageInterface $message): void |
|
98 | + { |
|
99 | + $body = $message->getBody(); |
|
100 | + |
|
101 | + if ($body->tell()) { |
|
102 | + $body->rewind(); |
|
103 | + } |
|
104 | + } |
|
105 | + |
|
106 | + /** |
|
107 | + * Parses an HTTP message into an associative array. |
|
108 | + * |
|
109 | + * The array contains the "start-line" key containing the start line of |
|
110 | + * the message, "headers" key containing an associative array of header |
|
111 | + * array values, and a "body" key containing the body of the message. |
|
112 | + * |
|
113 | + * @param string $message HTTP request or response to parse. |
|
114 | + */ |
|
115 | + public static function parseMessage(string $message): array |
|
116 | + { |
|
117 | + if (!$message) { |
|
118 | + throw new \InvalidArgumentException('Invalid message'); |
|
119 | + } |
|
120 | + |
|
121 | + $message = ltrim($message, "\r\n"); |
|
122 | + |
|
123 | + $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); |
|
124 | + |
|
125 | + if ($messageParts === false || count($messageParts) !== 2) { |
|
126 | + throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); |
|
127 | + } |
|
128 | + |
|
129 | + [$rawHeaders, $body] = $messageParts; |
|
130 | + $rawHeaders .= "\r\n"; // Put back the delimiter we split previously |
|
131 | + $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); |
|
132 | + |
|
133 | + if ($headerParts === false || count($headerParts) !== 2) { |
|
134 | + throw new \InvalidArgumentException('Invalid message: Missing status line'); |
|
135 | + } |
|
136 | + |
|
137 | + [$startLine, $rawHeaders] = $headerParts; |
|
138 | + |
|
139 | + if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { |
|
140 | + // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 |
|
141 | + $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); |
|
142 | + } |
|
143 | + |
|
144 | + /** @var array[] $headerLines */ |
|
145 | + $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); |
|
146 | + |
|
147 | + // If these aren't the same, then one line didn't match and there's an invalid header. |
|
148 | + if ($count !== substr_count($rawHeaders, "\n")) { |
|
149 | + // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 |
|
150 | + if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { |
|
151 | + throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); |
|
152 | + } |
|
153 | + |
|
154 | + throw new \InvalidArgumentException('Invalid header syntax'); |
|
155 | + } |
|
156 | + |
|
157 | + $headers = []; |
|
158 | + |
|
159 | + foreach ($headerLines as $headerLine) { |
|
160 | + $headers[$headerLine[1]][] = $headerLine[2]; |
|
161 | + } |
|
162 | + |
|
163 | + return [ |
|
164 | + 'start-line' => $startLine, |
|
165 | + 'headers' => $headers, |
|
166 | + 'body' => $body, |
|
167 | + ]; |
|
168 | + } |
|
169 | + |
|
170 | + /** |
|
171 | + * Constructs a URI for an HTTP request message. |
|
172 | + * |
|
173 | + * @param string $path Path from the start-line |
|
174 | + * @param array $headers Array of headers (each value an array). |
|
175 | + */ |
|
176 | + public static function parseRequestUri(string $path, array $headers): string |
|
177 | + { |
|
178 | + $hostKey = array_filter(array_keys($headers), function ($k) { |
|
179 | + // Numeric array keys are converted to int by PHP. |
|
180 | + $k = (string) $k; |
|
181 | + |
|
182 | + return strtolower($k) === 'host'; |
|
183 | + }); |
|
184 | + |
|
185 | + // If no host is found, then a full URI cannot be constructed. |
|
186 | + if (!$hostKey) { |
|
187 | + return $path; |
|
188 | + } |
|
189 | + |
|
190 | + $host = $headers[reset($hostKey)][0]; |
|
191 | + $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; |
|
192 | + |
|
193 | + return $scheme.'://'.$host.'/'.ltrim($path, '/'); |
|
194 | + } |
|
195 | + |
|
196 | + /** |
|
197 | + * Parses a request message string into a request object. |
|
198 | + * |
|
199 | + * @param string $message Request message string. |
|
200 | + */ |
|
201 | + public static function parseRequest(string $message): RequestInterface |
|
202 | + { |
|
203 | + $data = self::parseMessage($message); |
|
204 | + $matches = []; |
|
205 | + if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { |
|
206 | + throw new \InvalidArgumentException('Invalid request string'); |
|
207 | + } |
|
208 | + $parts = explode(' ', $data['start-line'], 3); |
|
209 | + $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; |
|
210 | + |
|
211 | + $request = new Request( |
|
212 | + $parts[0], |
|
213 | + $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], |
|
214 | + $data['headers'], |
|
215 | + $data['body'], |
|
216 | + $version |
|
217 | + ); |
|
218 | + |
|
219 | + return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); |
|
220 | + } |
|
221 | + |
|
222 | + /** |
|
223 | + * Parses a response message string into a response object. |
|
224 | + * |
|
225 | + * @param string $message Response message string. |
|
226 | + */ |
|
227 | + public static function parseResponse(string $message): ResponseInterface |
|
228 | + { |
|
229 | + $data = self::parseMessage($message); |
|
230 | + // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 |
|
231 | + // the space between status-code and reason-phrase is required. But |
|
232 | + // browsers accept responses without space and reason as well. |
|
233 | + if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { |
|
234 | + throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']); |
|
235 | + } |
|
236 | + $parts = explode(' ', $data['start-line'], 3); |
|
237 | + |
|
238 | + return new Response( |
|
239 | + (int) $parts[1], |
|
240 | + $data['headers'], |
|
241 | + $data['body'], |
|
242 | + explode('/', $parts[0])[1], |
|
243 | + $parts[2] ?? null |
|
244 | + ); |
|
245 | + } |
|
246 | 246 | } |
@@ -175,9 +175,9 @@ discard block |
||
175 | 175 | */ |
176 | 176 | public static function parseRequestUri(string $path, array $headers): string |
177 | 177 | { |
178 | - $hostKey = array_filter(array_keys($headers), function ($k) { |
|
178 | + $hostKey = array_filter(array_keys($headers), function($k) { |
|
179 | 179 | // Numeric array keys are converted to int by PHP. |
180 | - $k = (string) $k; |
|
180 | + $k = (string)$k; |
|
181 | 181 | |
182 | 182 | return strtolower($k) === 'host'; |
183 | 183 | }); |
@@ -236,7 +236,7 @@ discard block |
||
236 | 236 | $parts = explode(' ', $data['start-line'], 3); |
237 | 237 | |
238 | 238 | return new Response( |
239 | - (int) $parts[1], |
|
239 | + (int)$parts[1], |
|
240 | 240 | $data['headers'], |
241 | 241 | $data['body'], |
242 | 242 | explode('/', $parts[0])[1], |
@@ -8,8 +8,7 @@ |
||
8 | 8 | use OCA\FullTextSearch_Elasticsearch\Vendor\Psr\Http\Message\RequestInterface; |
9 | 9 | use OCA\FullTextSearch_Elasticsearch\Vendor\Psr\Http\Message\ResponseInterface; |
10 | 10 | |
11 | -final class Message |
|
12 | -{ |
|
11 | +final class Message { |
|
13 | 12 | /** |
14 | 13 | * Returns the string representation of an HTTP message. |
15 | 14 | * |