1<?php 2namespace TYPO3\CMS\Core\Http; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Psr\Http\Message\MessageInterface; 18use Psr\Http\Message\StreamInterface; 19 20/** 21 * Default implementation for the MessageInterface of the PSR-7 standard 22 * It is the base for any request or response for PSR-7. 23 * 24 * Highly inspired by https://github.com/phly/http/ 25 * 26 * @internal Note that this is not public API yet. 27 */ 28class Message implements MessageInterface 29{ 30 /** 31 * The HTTP Protocol version, defaults to 1.1 32 * @var string 33 */ 34 protected $protocolVersion = '1.1'; 35 36 /** 37 * Associative array containing all headers of this Message 38 * This is a mixed-case list of the headers (as due to the specification) 39 * @var array 40 */ 41 protected $headers = []; 42 43 /** 44 * Lowercased version of all headers, in order to check if a header is set or not 45 * this way a lot of checks are easier to be set 46 * @var array 47 */ 48 protected $lowercasedHeaderNames = []; 49 50 /** 51 * The body as a Stream object 52 * @var StreamInterface 53 */ 54 protected $body; 55 56 /** 57 * Retrieves the HTTP protocol version as a string. 58 * 59 * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). 60 * 61 * @return string HTTP protocol version. 62 */ 63 public function getProtocolVersion() 64 { 65 return $this->protocolVersion; 66 } 67 68 /** 69 * Return an instance with the specified HTTP protocol version. 70 * 71 * The version string MUST contain only the HTTP version number (e.g., 72 * "1.1", "1.0"). 73 * 74 * This method MUST be implemented in such a way as to retain the 75 * immutability of the message, and MUST return an instance that has the 76 * new protocol version. 77 * 78 * @param string $version HTTP protocol version 79 * @return static 80 */ 81 public function withProtocolVersion($version) 82 { 83 $clonedObject = clone $this; 84 $clonedObject->protocolVersion = $version; 85 return $clonedObject; 86 } 87 88 /** 89 * Retrieves all message header values. 90 * 91 * The keys represent the header name as it will be sent over the wire, and 92 * each value is an array of strings associated with the header. 93 * 94 * // Represent the headers as a string 95 * foreach ($message->getHeaders() as $name => $values) { 96 * echo $name . ": " . implode(", ", $values); 97 * } 98 * 99 * // Emit headers iteratively: 100 * foreach ($message->getHeaders() as $name => $values) { 101 * foreach ($values as $value) { 102 * header(sprintf('%s: %s', $name, $value), false); 103 * } 104 * } 105 * 106 * While header names are not case-sensitive, getHeaders() will preserve the 107 * exact case in which headers were originally specified. 108 * 109 * @return array Returns an associative array of the message's headers. Each 110 * key MUST be a header name, and each value MUST be an array of strings 111 * for that header. 112 */ 113 public function getHeaders() 114 { 115 return $this->headers; 116 } 117 118 /** 119 * Checks if a header exists by the given case-insensitive name. 120 * 121 * @param string $name Case-insensitive header field name. 122 * @return bool Returns true if any header names match the given header 123 * name using a case-insensitive string comparison. Returns false if 124 * no matching header name is found in the message. 125 */ 126 public function hasHeader($name) 127 { 128 return isset($this->lowercasedHeaderNames[strtolower($name)]); 129 } 130 131 /** 132 * Retrieves a message header value by the given case-insensitive name. 133 * 134 * This method returns an array of all the header values of the given 135 * case-insensitive header name. 136 * 137 * If the header does not appear in the message, this method MUST return an 138 * empty array. 139 * 140 * @param string $name Case-insensitive header field name. 141 * @return string[] An array of string values as provided for the given 142 * header. If the header does not appear in the message, this method MUST 143 * return an empty array. 144 */ 145 public function getHeader($name) 146 { 147 if (!$this->hasHeader($name)) { 148 return []; 149 } 150 $header = $this->lowercasedHeaderNames[strtolower($name)]; 151 $headerValue = $this->headers[$header]; 152 if (is_array($headerValue)) { 153 return $headerValue; 154 } 155 return [$headerValue]; 156 } 157 158 /** 159 * Retrieves a comma-separated string of the values for a single header. 160 * 161 * This method returns all of the header values of the given 162 * case-insensitive header name as a string concatenated together using 163 * a comma. 164 * 165 * NOTE: Not all header values may be appropriately represented using 166 * comma concatenation. For such headers, use getHeader() instead 167 * and supply your own delimiter when concatenating. 168 * 169 * If the header does not appear in the message, this method MUST return 170 * an empty string. 171 * 172 * @param string $name Case-insensitive header field name. 173 * @return string A string of values as provided for the given header 174 * concatenated together using a comma. If the header does not appear in 175 * the message, this method MUST return an empty string. 176 */ 177 public function getHeaderLine($name) 178 { 179 $headerValue = $this->getHeader($name); 180 if (empty($headerValue)) { 181 return ''; 182 } 183 return implode(',', $headerValue); 184 } 185 186 /** 187 * Return an instance with the provided value replacing the specified header. 188 * 189 * While header names are case-insensitive, the casing of the header will 190 * be preserved by this function, and returned from getHeaders(). 191 * 192 * This method MUST be implemented in such a way as to retain the 193 * immutability of the message, and MUST return an instance that has the 194 * new and/or updated header and value. 195 * 196 * @param string $name Case-insensitive header field name. 197 * @param string|string[] $value Header value(s). 198 * @return static 199 * @throws \InvalidArgumentException for invalid header names or values. 200 */ 201 public function withHeader($name, $value) 202 { 203 if (is_string($value)) { 204 $value = [$value]; 205 } 206 207 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) { 208 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The value must be a string or an array of strings.', 1436717266); 209 } 210 211 $this->validateHeaderName($name); 212 $this->validateHeaderValues($value); 213 $lowercasedHeaderName = strtolower($name); 214 215 $clonedObject = clone $this; 216 $clonedObject->headers[$name] = $value; 217 $clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name; 218 return $clonedObject; 219 } 220 221 /** 222 * Return an instance with the specified header appended with the given value. 223 * 224 * Existing values for the specified header will be maintained. The new 225 * value(s) will be appended to the existing list. If the header did not 226 * exist previously, it will be added. 227 * 228 * This method MUST be implemented in such a way as to retain the 229 * immutability of the message, and MUST return an instance that has the 230 * new header and/or value. 231 * 232 * @param string $name Case-insensitive header field name to add. 233 * @param string|string[] $value Header value(s). 234 * @return static 235 * @throws \InvalidArgumentException for invalid header names or values. 236 */ 237 public function withAddedHeader($name, $value) 238 { 239 if (is_string($value)) { 240 $value = [$value]; 241 } 242 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) { 243 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267); 244 } 245 $this->validateHeaderName($name); 246 $this->validateHeaderValues($value); 247 if (!$this->hasHeader($name)) { 248 return $this->withHeader($name, $value); 249 } 250 $name = $this->lowercasedHeaderNames[strtolower($name)]; 251 $clonedObject = clone $this; 252 $clonedObject->headers[$name] = array_merge($this->headers[$name], $value); 253 return $clonedObject; 254 } 255 256 /** 257 * Return an instance without the specified header. 258 * 259 * Header resolution MUST be done without case-sensitivity. 260 * 261 * This method MUST be implemented in such a way as to retain the 262 * immutability of the message, and MUST return an instance that removes 263 * the named header. 264 * 265 * @param string $name Case-insensitive header field name to remove. 266 * @return static 267 */ 268 public function withoutHeader($name) 269 { 270 if (!$this->hasHeader($name)) { 271 return clone $this; 272 } 273 // fetch the original header from the lowercased version 274 $lowercasedHeader = strtolower($name); 275 $name = $this->lowercasedHeaderNames[$lowercasedHeader]; 276 $clonedObject = clone $this; 277 unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]); 278 return $clonedObject; 279 } 280 281 /** 282 * Gets the body of the message. 283 * 284 * @return \Psr\Http\Message\StreamInterface Returns the body as a stream. 285 */ 286 public function getBody() 287 { 288 return $this->body; 289 } 290 291 /** 292 * Return an instance with the specified message body. 293 * 294 * The body MUST be a StreamInterface object. 295 * 296 * This method MUST be implemented in such a way as to retain the 297 * immutability of the message, and MUST return a new instance that has the 298 * new body stream. 299 * 300 * @param \Psr\Http\Message\StreamInterface $body Body. 301 * @return static 302 * @throws \InvalidArgumentException When the body is not valid. 303 */ 304 public function withBody(StreamInterface $body) 305 { 306 $clonedObject = clone $this; 307 $clonedObject->body = $body; 308 return $clonedObject; 309 } 310 311 /** 312 * Ensure header names and values are valid. 313 * 314 * @param array $headers 315 * @throws \InvalidArgumentException 316 */ 317 protected function assertHeaders(array $headers) 318 { 319 foreach ($headers as $name => $headerValues) { 320 $this->validateHeaderName($name); 321 // check if all values are correct 322 array_walk($headerValues, function ($value, $key, Message $messageObject) { 323 if (!$messageObject->isValidHeaderValue($value)) { 324 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268); 325 } 326 }, $this); 327 } 328 } 329 330 /** 331 * Filter a set of headers to ensure they are in the correct internal format. 332 * 333 * Used by message constructors to allow setting all initial headers at once. 334 * 335 * @param array $originalHeaders Headers to filter. 336 * @return array Filtered headers and names. 337 */ 338 protected function filterHeaders(array $originalHeaders) 339 { 340 $headerNames = $headers = []; 341 foreach ($originalHeaders as $header => $value) { 342 if (!is_string($header) || (!is_array($value) && !is_string($value))) { 343 continue; 344 } 345 if (!is_array($value)) { 346 $value = [$value]; 347 } 348 $headerNames[strtolower($header)] = $header; 349 $headers[$header] = $value; 350 } 351 return [$headerNames, $headers]; 352 } 353 354 /** 355 * Helper function to test if an array contains only strings 356 * 357 * @param array $data 358 * @return bool 359 */ 360 protected function arrayContainsOnlyStrings(array $data) 361 { 362 return array_reduce($data, function ($original, $item) { 363 return is_string($item) ? $original : false; 364 }, true); 365 } 366 367 /** 368 * Assert that the provided header values are valid. 369 * 370 * @see http://tools.ietf.org/html/rfc7230#section-3.2 371 * @param string[] $values 372 * @throws \InvalidArgumentException 373 */ 374 protected function validateHeaderValues(array $values) 375 { 376 array_walk($values, function ($value, $key, Message $messageObject) { 377 if (!$messageObject->isValidHeaderValue($value)) { 378 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269); 379 } 380 }, $this); 381 } 382 383 /** 384 * Filter a header value 385 * 386 * Ensures CRLF header injection vectors are filtered. 387 * 388 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal 389 * tabs are allowed in values; header continuations MUST consist of 390 * a single CRLF sequence followed by a space or horizontal tab. 391 * 392 * This method filters any values not allowed from the string, and is 393 * lossy. 394 * 395 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting 396 * @param string $value 397 * @return string 398 */ 399 public function filter($value) 400 { 401 $value = (string)$value; 402 $length = strlen($value); 403 $string = ''; 404 for ($i = 0; $i < $length; $i += 1) { 405 $ascii = ord($value[$i]); 406 407 // Detect continuation sequences 408 if ($ascii === 13) { 409 $lf = ord($value[$i + 1]); 410 $ws = ord($value[$i + 2]); 411 if ($lf === 10 && in_array($ws, [9, 32], true)) { 412 $string .= $value[$i] . $value[$i + 1]; 413 $i += 1; 414 } 415 continue; 416 } 417 418 // Non-visible, non-whitespace characters 419 // 9 === horizontal tab 420 // 32-126, 128-254 === visible 421 // 127 === DEL 422 // 255 === null byte 423 if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) { 424 continue; 425 } 426 427 $string .= $value[$i]; 428 } 429 430 return $string; 431 } 432 433 /** 434 * Check whether or not a header name is valid and throw an exception. 435 * 436 * @see http://tools.ietf.org/html/rfc7230#section-3.2 437 * @param string $name 438 * @throws \InvalidArgumentException 439 */ 440 public function validateHeaderName($name) 441 { 442 if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) { 443 throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270); 444 } 445 } 446 447 /** 448 * Checks if an HTTP header value is valid. 449 * 450 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal 451 * tabs are allowed in values; header continuations MUST consist of 452 * a single CRLF sequence followed by a space or horizontal tab. 453 * 454 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting 455 * @param string $value 456 * @return bool 457 */ 458 public function isValidHeaderValue($value) 459 { 460 $value = (string)$value; 461 462 // Any occurrence of \r or \n is invalid 463 if (strpbrk($value, "\r\n") !== false) { 464 return false; 465 } 466 467 $length = strlen($value); 468 for ($i = 0; $i < $length; $i += 1) { 469 $ascii = ord($value[$i]); 470 471 // Non-visible, non-whitespace characters 472 // 9 === horizontal tab 473 // 10 === line feed 474 // 13 === carriage return 475 // 32-126, 128-254 === visible 476 // 127 === DEL 477 // 255 === null byte 478 if (($ascii < 32 && !in_array($ascii, [9, 10, 13], true)) || $ascii === 127 || $ascii > 254) { 479 return false; 480 } 481 } 482 483 return true; 484 } 485} 486