1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * An abstract base MIME Header.
13 *
14 * @author Chris Corbyn
15 */
16class Swift_Mime_Headers_ParameterizedHeader extends Swift_Mime_Headers_UnstructuredHeader implements Swift_Mime_ParameterizedHeader
17{
18    /**
19     * RFC 2231's definition of a token.
20     *
21     * @var string
22     */
23    const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)';
24
25    /**
26     * The Encoder used to encode the parameters.
27     *
28     * @var Swift_Encoder
29     */
30    private $_paramEncoder;
31
32    /**
33     * The parameters as an associative array.
34     *
35     * @var string[]
36     */
37    private $_params = array();
38
39    /**
40     * Creates a new ParameterizedHeader with $name.
41     *
42     * @param string                   $name
43     * @param Swift_Mime_HeaderEncoder $encoder
44     * @param Swift_Encoder            $paramEncoder, optional
45     * @param Swift_Mime_Grammar       $grammar
46     */
47    public function __construct($name, Swift_Mime_HeaderEncoder $encoder, Swift_Encoder $paramEncoder = null, Swift_Mime_Grammar $grammar)
48    {
49        parent::__construct($name, $encoder, $grammar);
50        $this->_paramEncoder = $paramEncoder;
51    }
52
53    /**
54     * Get the type of Header that this instance represents.
55     *
56     * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX
57     * @see TYPE_DATE, TYPE_ID, TYPE_PATH
58     *
59     * @return int
60     */
61    public function getFieldType()
62    {
63        return self::TYPE_PARAMETERIZED;
64    }
65
66    /**
67     * Set the character set used in this Header.
68     *
69     * @param string $charset
70     */
71    public function setCharset($charset)
72    {
73        parent::setCharset($charset);
74        if (isset($this->_paramEncoder)) {
75            $this->_paramEncoder->charsetChanged($charset);
76        }
77    }
78
79    /**
80     * Set the value of $parameter.
81     *
82     * @param string $parameter
83     * @param string $value
84     */
85    public function setParameter($parameter, $value)
86    {
87        $this->setParameters(array_merge($this->getParameters(), array($parameter => $value)));
88    }
89
90    /**
91     * Get the value of $parameter.
92     *
93     * @param string $parameter
94     *
95     * @return string
96     */
97    public function getParameter($parameter)
98    {
99        $params = $this->getParameters();
100
101        return array_key_exists($parameter, $params) ? $params[$parameter] : null;
102    }
103
104    /**
105     * Set an associative array of parameter names mapped to values.
106     *
107     * @param string[] $parameters
108     */
109    public function setParameters(array $parameters)
110    {
111        $this->clearCachedValueIf($this->_params != $parameters);
112        $this->_params = $parameters;
113    }
114
115    /**
116     * Returns an associative array of parameter names mapped to values.
117     *
118     * @return string[]
119     */
120    public function getParameters()
121    {
122        return $this->_params;
123    }
124
125    /**
126     * Get the value of this header prepared for rendering.
127     *
128     * @return string
129     */
130    public function getFieldBody() //TODO: Check caching here
131    {
132        $body = parent::getFieldBody();
133        foreach ($this->_params as $name => $value) {
134            if (null !== $value) {
135                // Add the parameter
136                $body .= '; '.$this->_createParameter($name, $value);
137            }
138        }
139
140        return $body;
141    }
142
143    /**
144     * Generate a list of all tokens in the final header.
145     *
146     * This doesn't need to be overridden in theory, but it is for implementation
147     * reasons to prevent potential breakage of attributes.
148     *
149     * @param string $string The string to tokenize
150     *
151     * @return array An array of tokens as strings
152     */
153    protected function toTokens($string = null)
154    {
155        $tokens = parent::toTokens(parent::getFieldBody());
156
157        // Try creating any parameters
158        foreach ($this->_params as $name => $value) {
159            if (null !== $value) {
160                // Add the semi-colon separator
161                $tokens[count($tokens) - 1] .= ';';
162                $tokens = array_merge($tokens, $this->generateTokenLines(
163                    ' '.$this->_createParameter($name, $value)
164                    ));
165            }
166        }
167
168        return $tokens;
169    }
170
171    /**
172     * Render a RFC 2047 compliant header parameter from the $name and $value.
173     *
174     * @param string $name
175     * @param string $value
176     *
177     * @return string
178     */
179    private function _createParameter($name, $value)
180    {
181        $origValue = $value;
182
183        $encoded = false;
184        // Allow room for parameter name, indices, "=" and DQUOTEs
185        $maxValueLength = $this->getMaxLineLength() - strlen($name.'=*N"";') - 1;
186        $firstLineOffset = 0;
187
188        // If it's not already a valid parameter value...
189        if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
190            // TODO: text, or something else??
191            // ... and it's not ascii
192            if (!preg_match('/^'.$this->getGrammar()->getDefinition('text').'*$/D', $value)) {
193                $encoded = true;
194                // Allow space for the indices, charset and language
195                $maxValueLength = $this->getMaxLineLength() - strlen($name.'*N*="";') - 1;
196                $firstLineOffset = strlen(
197                    $this->getCharset()."'".$this->getLanguage()."'"
198                    );
199            }
200        }
201
202        // Encode if we need to
203        if ($encoded || strlen($value) > $maxValueLength) {
204            if (isset($this->_paramEncoder)) {
205                $value = $this->_paramEncoder->encodeString(
206                    $origValue, $firstLineOffset, $maxValueLength, $this->getCharset()
207                    );
208            } else {
209                // We have to go against RFC 2183/2231 in some areas for interoperability
210                $value = $this->getTokenAsEncodedWord($origValue);
211                $encoded = false;
212            }
213        }
214
215        $valueLines = isset($this->_paramEncoder) ? explode("\r\n", $value) : array($value);
216
217        // Need to add indices
218        if (count($valueLines) > 1) {
219            $paramLines = array();
220            foreach ($valueLines as $i => $line) {
221                $paramLines[] = $name.'*'.$i.
222                    $this->_getEndOfParameterValue($line, true, $i == 0);
223            }
224
225            return implode(";\r\n ", $paramLines);
226        } else {
227            return $name.$this->_getEndOfParameterValue(
228                $valueLines[0], $encoded, true
229                );
230        }
231    }
232
233    /**
234     * Returns the parameter value from the "=" and beyond.
235     *
236     * @param string $value     to append
237     * @param bool   $encoded
238     * @param bool   $firstLine
239     *
240     * @return string
241     */
242    private function _getEndOfParameterValue($value, $encoded = false, $firstLine = false)
243    {
244        if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
245            $value = '"'.$value.'"';
246        }
247        $prepend = '=';
248        if ($encoded) {
249            $prepend = '*=';
250            if ($firstLine) {
251                $prepend = '*='.$this->getCharset()."'".$this->getLanguage().
252                    "'";
253            }
254        }
255
256        return $prepend.$value;
257    }
258}
259