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