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