friendship ended with social-app. php is my new best friend
1<?php
2
3declare(strict_types=1);
4
5namespace GuzzleHttp\Psr7;
6
7use Psr\Http\Message\MessageInterface;
8use Psr\Http\Message\StreamInterface;
9
10/**
11 * Trait implementing functionality common to requests and responses.
12 */
13trait MessageTrait
14{
15 /** @var string[][] Map of all registered headers, as original name => array of values */
16 private $headers = [];
17
18 /** @var string[] Map of lowercase header name => original name at registration */
19 private $headerNames = [];
20
21 /** @var string */
22 private $protocol = '1.1';
23
24 /** @var StreamInterface|null */
25 private $stream;
26
27 public function getProtocolVersion(): string
28 {
29 return $this->protocol;
30 }
31
32 public function withProtocolVersion($version): MessageInterface
33 {
34 if ($this->protocol === $version) {
35 return $this;
36 }
37
38 $new = clone $this;
39 $new->protocol = $version;
40
41 return $new;
42 }
43
44 public function getHeaders(): array
45 {
46 return $this->headers;
47 }
48
49 public function hasHeader($header): bool
50 {
51 return isset($this->headerNames[strtolower($header)]);
52 }
53
54 public function getHeader($header): array
55 {
56 $header = strtolower($header);
57
58 if (!isset($this->headerNames[$header])) {
59 return [];
60 }
61
62 $header = $this->headerNames[$header];
63
64 return $this->headers[$header];
65 }
66
67 public function getHeaderLine($header): string
68 {
69 return implode(', ', $this->getHeader($header));
70 }
71
72 public function withHeader($header, $value): MessageInterface
73 {
74 $this->assertHeader($header);
75 $value = $this->normalizeHeaderValue($value);
76 $normalized = strtolower($header);
77
78 $new = clone $this;
79 if (isset($new->headerNames[$normalized])) {
80 unset($new->headers[$new->headerNames[$normalized]]);
81 }
82 $new->headerNames[$normalized] = $header;
83 $new->headers[$header] = $value;
84
85 return $new;
86 }
87
88 public function withAddedHeader($header, $value): MessageInterface
89 {
90 $this->assertHeader($header);
91 $value = $this->normalizeHeaderValue($value);
92 $normalized = strtolower($header);
93
94 $new = clone $this;
95 if (isset($new->headerNames[$normalized])) {
96 $header = $this->headerNames[$normalized];
97 $new->headers[$header] = array_merge($this->headers[$header], $value);
98 } else {
99 $new->headerNames[$normalized] = $header;
100 $new->headers[$header] = $value;
101 }
102
103 return $new;
104 }
105
106 public function withoutHeader($header): MessageInterface
107 {
108 $normalized = strtolower($header);
109
110 if (!isset($this->headerNames[$normalized])) {
111 return $this;
112 }
113
114 $header = $this->headerNames[$normalized];
115
116 $new = clone $this;
117 unset($new->headers[$header], $new->headerNames[$normalized]);
118
119 return $new;
120 }
121
122 public function getBody(): StreamInterface
123 {
124 if (!$this->stream) {
125 $this->stream = Utils::streamFor('');
126 }
127
128 return $this->stream;
129 }
130
131 public function withBody(StreamInterface $body): MessageInterface
132 {
133 if ($body === $this->stream) {
134 return $this;
135 }
136
137 $new = clone $this;
138 $new->stream = $body;
139
140 return $new;
141 }
142
143 /**
144 * @param (string|string[])[] $headers
145 */
146 private function setHeaders(array $headers): void
147 {
148 $this->headerNames = $this->headers = [];
149 foreach ($headers as $header => $value) {
150 // Numeric array keys are converted to int by PHP.
151 $header = (string) $header;
152
153 $this->assertHeader($header);
154 $value = $this->normalizeHeaderValue($value);
155 $normalized = strtolower($header);
156 if (isset($this->headerNames[$normalized])) {
157 $header = $this->headerNames[$normalized];
158 $this->headers[$header] = array_merge($this->headers[$header], $value);
159 } else {
160 $this->headerNames[$normalized] = $header;
161 $this->headers[$header] = $value;
162 }
163 }
164 }
165
166 /**
167 * @param mixed $value
168 *
169 * @return string[]
170 */
171 private function normalizeHeaderValue($value): array
172 {
173 if (!is_array($value)) {
174 return $this->trimAndValidateHeaderValues([$value]);
175 }
176
177 return $this->trimAndValidateHeaderValues($value);
178 }
179
180 /**
181 * Trims whitespace from the header values.
182 *
183 * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field.
184 *
185 * header-field = field-name ":" OWS field-value OWS
186 * OWS = *( SP / HTAB )
187 *
188 * @param mixed[] $values Header values
189 *
190 * @return string[] Trimmed header values
191 *
192 * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
193 */
194 private function trimAndValidateHeaderValues(array $values): array
195 {
196 return array_map(function ($value) {
197 if (!is_scalar($value) && null !== $value) {
198 throw new \InvalidArgumentException(sprintf(
199 'Header value must be scalar or null but %s provided.',
200 is_object($value) ? get_class($value) : gettype($value)
201 ));
202 }
203
204 $trimmed = trim((string) $value, " \t");
205 $this->assertValue($trimmed);
206
207 return $trimmed;
208 }, array_values($values));
209 }
210
211 /**
212 * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
213 *
214 * @param mixed $header
215 */
216 private function assertHeader($header): void
217 {
218 if (!is_string($header)) {
219 throw new \InvalidArgumentException(sprintf(
220 'Header name must be a string but %s provided.',
221 is_object($header) ? get_class($header) : gettype($header)
222 ));
223 }
224
225 if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) {
226 throw new \InvalidArgumentException(
227 sprintf('"%s" is not valid header name.', $header)
228 );
229 }
230 }
231
232 /**
233 * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
234 *
235 * field-value = *( field-content / obs-fold )
236 * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
237 * field-vchar = VCHAR / obs-text
238 * VCHAR = %x21-7E
239 * obs-text = %x80-FF
240 * obs-fold = CRLF 1*( SP / HTAB )
241 */
242 private function assertValue(string $value): void
243 {
244 // The regular expression intentionally does not support the obs-fold production, because as
245 // per RFC 7230#3.2.4:
246 //
247 // A sender MUST NOT generate a message that includes
248 // line folding (i.e., that has any field-value that contains a match to
249 // the obs-fold rule) unless the message is intended for packaging
250 // within the message/http media type.
251 //
252 // Clients must not send a request with line folding and a server sending folded headers is
253 // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
254 // folding is not likely to break any legitimate use case.
255 if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
256 throw new \InvalidArgumentException(
257 sprintf('"%s" is not valid header value.', $value)
258 );
259 }
260 }
261}