MessageTrait.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. declare(strict_types=1);
  3. namespace Nyholm\Psr7;
  4. use Psr\Http\Message\MessageInterface;
  5. use Psr\Http\Message\StreamInterface;
  6. /**
  7. * Trait implementing functionality common to requests and responses.
  8. *
  9. * @author Michael Dowling and contributors to guzzlehttp/psr7
  10. * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  11. * @author Martijn van der Ven <martijn@vanderven.se>
  12. *
  13. * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
  14. */
  15. trait MessageTrait
  16. {
  17. /** @var array Map of all registered headers, as original name => array of values */
  18. private $headers = [];
  19. /** @var array Map of lowercase header name => original name at registration */
  20. private $headerNames = [];
  21. /** @var string */
  22. private $protocol = '1.1';
  23. /** @var StreamInterface|null */
  24. private $stream;
  25. public function getProtocolVersion(): string
  26. {
  27. return $this->protocol;
  28. }
  29. /**
  30. * @return static
  31. */
  32. public function withProtocolVersion($version): MessageInterface
  33. {
  34. if (!\is_scalar($version)) {
  35. throw new \InvalidArgumentException('Protocol version must be a string');
  36. }
  37. if ($this->protocol === $version) {
  38. return $this;
  39. }
  40. $new = clone $this;
  41. $new->protocol = (string) $version;
  42. return $new;
  43. }
  44. public function getHeaders(): array
  45. {
  46. return $this->headers;
  47. }
  48. public function hasHeader($header): bool
  49. {
  50. return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]);
  51. }
  52. public function getHeader($header): array
  53. {
  54. if (!\is_string($header)) {
  55. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
  56. }
  57. $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
  58. if (!isset($this->headerNames[$header])) {
  59. return [];
  60. }
  61. $header = $this->headerNames[$header];
  62. return $this->headers[$header];
  63. }
  64. public function getHeaderLine($header): string
  65. {
  66. return \implode(', ', $this->getHeader($header));
  67. }
  68. /**
  69. * @return static
  70. */
  71. public function withHeader($header, $value): MessageInterface
  72. {
  73. $value = $this->validateAndTrimHeader($header, $value);
  74. $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
  75. $new = clone $this;
  76. if (isset($new->headerNames[$normalized])) {
  77. unset($new->headers[$new->headerNames[$normalized]]);
  78. }
  79. $new->headerNames[$normalized] = $header;
  80. $new->headers[$header] = $value;
  81. return $new;
  82. }
  83. /**
  84. * @return static
  85. */
  86. public function withAddedHeader($header, $value): MessageInterface
  87. {
  88. if (!\is_string($header) || '' === $header) {
  89. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
  90. }
  91. $new = clone $this;
  92. $new->setHeaders([$header => $value]);
  93. return $new;
  94. }
  95. /**
  96. * @return static
  97. */
  98. public function withoutHeader($header): MessageInterface
  99. {
  100. if (!\is_string($header)) {
  101. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
  102. }
  103. $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
  104. if (!isset($this->headerNames[$normalized])) {
  105. return $this;
  106. }
  107. $header = $this->headerNames[$normalized];
  108. $new = clone $this;
  109. unset($new->headers[$header], $new->headerNames[$normalized]);
  110. return $new;
  111. }
  112. public function getBody(): StreamInterface
  113. {
  114. if (null === $this->stream) {
  115. $this->stream = Stream::create('');
  116. }
  117. return $this->stream;
  118. }
  119. /**
  120. * @return static
  121. */
  122. public function withBody(StreamInterface $body): MessageInterface
  123. {
  124. if ($body === $this->stream) {
  125. return $this;
  126. }
  127. $new = clone $this;
  128. $new->stream = $body;
  129. return $new;
  130. }
  131. private function setHeaders(array $headers): void
  132. {
  133. foreach ($headers as $header => $value) {
  134. if (\is_int($header)) {
  135. // If a header name was set to a numeric string, PHP will cast the key to an int.
  136. // We must cast it back to a string in order to comply with validation.
  137. $header = (string) $header;
  138. }
  139. $value = $this->validateAndTrimHeader($header, $value);
  140. $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
  141. if (isset($this->headerNames[$normalized])) {
  142. $header = $this->headerNames[$normalized];
  143. $this->headers[$header] = \array_merge($this->headers[$header], $value);
  144. } else {
  145. $this->headerNames[$normalized] = $header;
  146. $this->headers[$header] = $value;
  147. }
  148. }
  149. }
  150. /**
  151. * Make sure the header complies with RFC 7230.
  152. *
  153. * Header names must be a non-empty string consisting of token characters.
  154. *
  155. * Header values must be strings consisting of visible characters with all optional
  156. * leading and trailing whitespace stripped. This method will always strip such
  157. * optional whitespace. Note that the method does not allow folding whitespace within
  158. * the values as this was deprecated for almost all instances by the RFC.
  159. *
  160. * header-field = field-name ":" OWS field-value OWS
  161. * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
  162. * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
  163. * OWS = *( SP / HTAB )
  164. * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
  165. *
  166. * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
  167. */
  168. private function validateAndTrimHeader($header, $values): array
  169. {
  170. if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) {
  171. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
  172. }
  173. if (!\is_array($values)) {
  174. // This is simple, just one value.
  175. if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
  176. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
  177. }
  178. return [\trim((string) $values, " \t")];
  179. }
  180. if (empty($values)) {
  181. throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given');
  182. }
  183. // Assert Non empty array
  184. $returnValues = [];
  185. foreach ($values as $v) {
  186. if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@D", (string) $v)) {
  187. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
  188. }
  189. $returnValues[] = \trim((string) $v, " \t");
  190. }
  191. return $returnValues;
  192. }
  193. }