AbstractSerializer.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <?php
  2. /**
  3. * Zend Framework (http://framework.zend.com/)
  4. *
  5. * @see http://github.com/zendframework/zend-diactoros for the canonical source repository
  6. * @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
  8. */
  9. namespace Zend\Diactoros;
  10. use Psr\Http\Message\StreamInterface;
  11. use UnexpectedValueException;
  12. use function array_pop;
  13. use function implode;
  14. use function ltrim;
  15. use function preg_match;
  16. use function sprintf;
  17. use function str_replace;
  18. use function ucwords;
  19. /**
  20. * Provides base functionality for request and response de/serialization
  21. * strategies, including functionality for retrieving a line at a time from
  22. * the message, splitting headers from the body, and serializing headers.
  23. */
  24. abstract class AbstractSerializer
  25. {
  26. const CR = "\r";
  27. const EOL = "\r\n";
  28. const LF = "\n";
  29. /**
  30. * Retrieve a single line from the stream.
  31. *
  32. * Retrieves a line from the stream; a line is defined as a sequence of
  33. * characters ending in a CRLF sequence.
  34. *
  35. * @param StreamInterface $stream
  36. * @return string
  37. * @throws UnexpectedValueException if the sequence contains a CR or LF in
  38. * isolation, or ends in a CR.
  39. */
  40. protected static function getLine(StreamInterface $stream)
  41. {
  42. $line = '';
  43. $crFound = false;
  44. while (! $stream->eof()) {
  45. $char = $stream->read(1);
  46. if ($crFound && $char === self::LF) {
  47. $crFound = false;
  48. break;
  49. }
  50. // CR NOT followed by LF
  51. if ($crFound && $char !== self::LF) {
  52. throw new UnexpectedValueException('Unexpected carriage return detected');
  53. }
  54. // LF in isolation
  55. if (! $crFound && $char === self::LF) {
  56. throw new UnexpectedValueException('Unexpected line feed detected');
  57. }
  58. // CR found; do not append
  59. if ($char === self::CR) {
  60. $crFound = true;
  61. continue;
  62. }
  63. // Any other character: append
  64. $line .= $char;
  65. }
  66. // CR found at end of stream
  67. if ($crFound) {
  68. throw new UnexpectedValueException("Unexpected end of headers");
  69. }
  70. return $line;
  71. }
  72. /**
  73. * Split the stream into headers and body content.
  74. *
  75. * Returns an array containing two elements
  76. *
  77. * - The first is an array of headers
  78. * - The second is a StreamInterface containing the body content
  79. *
  80. * @param StreamInterface $stream
  81. * @return array
  82. * @throws UnexpectedValueException For invalid headers.
  83. */
  84. protected static function splitStream(StreamInterface $stream)
  85. {
  86. $headers = [];
  87. $currentHeader = false;
  88. while ($line = self::getLine($stream)) {
  89. if (preg_match(';^(?P<name>[!#$%&\'*+.^_`\|~0-9a-zA-Z-]+):(?P<value>.*)$;', $line, $matches)) {
  90. $currentHeader = $matches['name'];
  91. if (! isset($headers[$currentHeader])) {
  92. $headers[$currentHeader] = [];
  93. }
  94. $headers[$currentHeader][] = ltrim($matches['value']);
  95. continue;
  96. }
  97. if (! $currentHeader) {
  98. throw new UnexpectedValueException('Invalid header detected');
  99. }
  100. if (! preg_match('#^[ \t]#', $line)) {
  101. throw new UnexpectedValueException('Invalid header continuation');
  102. }
  103. // Append continuation to last header value found
  104. $value = array_pop($headers[$currentHeader]);
  105. $headers[$currentHeader][] = $value . ltrim($line);
  106. }
  107. // use RelativeStream to avoid copying initial stream into memory
  108. return [$headers, new RelativeStream($stream, $stream->tell())];
  109. }
  110. /**
  111. * Serialize headers to string values.
  112. *
  113. * @param array $headers
  114. * @return string
  115. */
  116. protected static function serializeHeaders(array $headers)
  117. {
  118. $lines = [];
  119. foreach ($headers as $header => $values) {
  120. $normalized = self::filterHeader($header);
  121. foreach ($values as $value) {
  122. $lines[] = sprintf('%s: %s', $normalized, $value);
  123. }
  124. }
  125. return implode("\r\n", $lines);
  126. }
  127. /**
  128. * Filter a header name to wordcase
  129. *
  130. * @param string $header
  131. * @return string
  132. */
  133. protected static function filterHeader($header)
  134. {
  135. $filtered = str_replace('-', ' ', $header);
  136. $filtered = ucwords($filtered);
  137. return str_replace(' ', '-', $filtered);
  138. }
  139. }