friendship ended with social-app. php is my new best friend
at main 7.6 kB view raw
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}