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