ParameterizedHeader.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Mime\Header;
  11. use Symfony\Component\Mime\Encoder\Rfc2231Encoder;
  12. /**
  13. * @author Chris Corbyn
  14. */
  15. final class ParameterizedHeader extends UnstructuredHeader
  16. {
  17. /**
  18. * RFC 2231's definition of a token.
  19. *
  20. * @var string
  21. */
  22. public const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)';
  23. private $encoder;
  24. private $parameters = [];
  25. public function __construct(string $name, string $value, array $parameters = [])
  26. {
  27. parent::__construct($name, $value);
  28. foreach ($parameters as $k => $v) {
  29. $this->setParameter($k, $v);
  30. }
  31. if ('content-type' !== strtolower($name)) {
  32. $this->encoder = new Rfc2231Encoder();
  33. }
  34. }
  35. public function setParameter(string $parameter, ?string $value)
  36. {
  37. $this->setParameters(array_merge($this->getParameters(), [$parameter => $value]));
  38. }
  39. public function getParameter(string $parameter): string
  40. {
  41. return $this->getParameters()[$parameter] ?? '';
  42. }
  43. /**
  44. * @param string[] $parameters
  45. */
  46. public function setParameters(array $parameters)
  47. {
  48. $this->parameters = $parameters;
  49. }
  50. /**
  51. * @return string[]
  52. */
  53. public function getParameters(): array
  54. {
  55. return $this->parameters;
  56. }
  57. public function getBodyAsString(): string
  58. {
  59. $body = parent::getBodyAsString();
  60. foreach ($this->parameters as $name => $value) {
  61. if (null !== $value) {
  62. $body .= '; '.$this->createParameter($name, $value);
  63. }
  64. }
  65. return $body;
  66. }
  67. /**
  68. * Generate a list of all tokens in the final header.
  69. *
  70. * This doesn't need to be overridden in theory, but it is for implementation
  71. * reasons to prevent potential breakage of attributes.
  72. */
  73. protected function toTokens(string $string = null): array
  74. {
  75. $tokens = parent::toTokens(parent::getBodyAsString());
  76. // Try creating any parameters
  77. foreach ($this->parameters as $name => $value) {
  78. if (null !== $value) {
  79. // Add the semi-colon separator
  80. $tokens[\count($tokens) - 1] .= ';';
  81. $tokens = array_merge($tokens, $this->generateTokenLines(' '.$this->createParameter($name, $value)));
  82. }
  83. }
  84. return $tokens;
  85. }
  86. /**
  87. * Render an RFC 2047 compliant header parameter from the $name and $value.
  88. */
  89. private function createParameter(string $name, string $value): string
  90. {
  91. $origValue = $value;
  92. $encoded = false;
  93. // Allow room for parameter name, indices, "=" and DQUOTEs
  94. $maxValueLength = $this->getMaxLineLength() - \strlen($name.'=*N"";') - 1;
  95. $firstLineOffset = 0;
  96. // If it's not already a valid parameter value...
  97. if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
  98. // TODO: text, or something else??
  99. // ... and it's not ascii
  100. if (!preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $value)) {
  101. $encoded = true;
  102. // Allow space for the indices, charset and language
  103. $maxValueLength = $this->getMaxLineLength() - \strlen($name.'*N*="";') - 1;
  104. $firstLineOffset = \strlen($this->getCharset()."'".$this->getLanguage()."'");
  105. }
  106. if (\in_array($name, ['name', 'filename'], true) && 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()) && preg_match('//u', $value)) {
  107. // WHATWG HTML living standard 4.10.21.8 2 specifies:
  108. // For field names and filenames for file fields, the result of the
  109. // encoding in the previous bullet point must be escaped by replacing
  110. // any 0x0A (LF) bytes with the byte sequence `%0A`, 0x0D (CR) with `%0D`
  111. // and 0x22 (") with `%22`.
  112. // The user agent must not perform any other escapes.
  113. $value = str_replace(['"', "\r", "\n"], ['%22', '%0D', '%0A'], $value);
  114. if (\strlen($value) <= $maxValueLength) {
  115. return $name.'="'.$value.'"';
  116. }
  117. $value = $origValue;
  118. }
  119. }
  120. // Encode if we need to
  121. if ($encoded || \strlen($value) > $maxValueLength) {
  122. if (null !== $this->encoder) {
  123. $value = $this->encoder->encodeString($origValue, $this->getCharset(), $firstLineOffset, $maxValueLength);
  124. } else {
  125. // We have to go against RFC 2183/2231 in some areas for interoperability
  126. $value = $this->getTokenAsEncodedWord($origValue);
  127. $encoded = false;
  128. }
  129. }
  130. $valueLines = $this->encoder ? explode("\r\n", $value) : [$value];
  131. // Need to add indices
  132. if (\count($valueLines) > 1) {
  133. $paramLines = [];
  134. foreach ($valueLines as $i => $line) {
  135. $paramLines[] = $name.'*'.$i.$this->getEndOfParameterValue($line, true, 0 === $i);
  136. }
  137. return implode(";\r\n ", $paramLines);
  138. } else {
  139. return $name.$this->getEndOfParameterValue($valueLines[0], $encoded, true);
  140. }
  141. }
  142. /**
  143. * Returns the parameter value from the "=" and beyond.
  144. *
  145. * @param string $value to append
  146. */
  147. private function getEndOfParameterValue(string $value, bool $encoded = false, bool $firstLine = false): string
  148. {
  149. $forceHttpQuoting = 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName());
  150. if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
  151. $value = '"'.$value.'"';
  152. }
  153. $prepend = '=';
  154. if ($encoded) {
  155. $prepend = '*=';
  156. if ($firstLine) {
  157. $prepend = '*='.$this->getCharset()."'".$this->getLanguage()."'";
  158. }
  159. }
  160. return $prepend.$value;
  161. }
  162. }