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