1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "net/http/http_content_disposition.h"
7 #include "base/base64.h"
8 #include "base/logging.h"
9 #include "base/strings/string_tokenizer.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "net/base/net_string_util.h"
14 #include "net/base/net_util.h"
15 #include "net/http/http_util.h"
19 enum RFC2047EncodingType
{
24 // Decodes a "Q" encoded string as described in RFC 2047 section 4.2. Similar to
25 // decoding a quoted-printable string. Returns true if the input was valid.
26 bool DecodeQEncoding(const std::string
& input
, std::string
* output
) {
28 temp
.reserve(input
.size());
29 for (std::string::const_iterator it
= input
.begin(); it
!= input
.end();
33 } else if (*it
== '=') {
34 if ((input
.end() - it
< 3) ||
35 !IsHexDigit(static_cast<unsigned char>(*(it
+ 1))) ||
36 !IsHexDigit(static_cast<unsigned char>(*(it
+ 2))))
38 unsigned char ch
= HexDigitToInt(*(it
+ 1)) * 16 +
39 HexDigitToInt(*(it
+ 2));
40 temp
.push_back(static_cast<char>(ch
));
43 } else if (0x20 < *it
&& *it
< 0x7F && *it
!= '?') {
44 // In a Q-encoded word, only printable ASCII characters
45 // represent themselves. Besides, space, '=', '_' and '?' are
46 // not allowed, but they're already filtered out.
59 // Decodes a "Q" or "B" encoded string as per RFC 2047 section 4. The encoding
60 // type is specified in |enc_type|.
61 bool DecodeBQEncoding(const std::string
& part
,
62 RFC2047EncodingType enc_type
,
63 const std::string
& charset
,
64 std::string
* output
) {
66 if (!((enc_type
== B_ENCODING
) ?
67 base::Base64Decode(part
, &decoded
) : DecodeQEncoding(part
, &decoded
))) {
71 if (decoded
.empty()) {
76 return net::ConvertToUtf8(decoded
, charset
.c_str(), output
);
79 bool DecodeWord(const std::string
& encoded_word
,
80 const std::string
& referrer_charset
,
83 int* parse_result_flags
) {
86 if (encoded_word
.empty())
89 if (!base::IsStringASCII(encoded_word
)) {
90 // Try UTF-8, referrer_charset and the native OS default charset in turn.
91 if (base::IsStringUTF8(encoded_word
)) {
92 *output
= encoded_word
;
94 base::string16 utf16_output
;
95 if (!referrer_charset
.empty() &&
96 net::ConvertToUTF16(encoded_word
, referrer_charset
.c_str(),
98 *output
= base::UTF16ToUTF8(utf16_output
);
100 *output
= base::WideToUTF8(base::SysNativeMBToWide(encoded_word
));
104 *parse_result_flags
|= net::HttpContentDisposition::HAS_NON_ASCII_STRINGS
;
108 // RFC 2047 : one of encoding methods supported by Firefox and relatively
109 // widely used by web servers.
110 // =?charset?<E>?<encoded string>?= where '<E>' is either 'B' or 'Q'.
111 // We don't care about the length restriction (72 bytes) because
112 // many web servers generate encoded words longer than the limit.
113 std::string decoded_word
;
117 base::StringTokenizer
t(encoded_word
, "?");
118 RFC2047EncodingType enc_type
= Q_ENCODING
;
119 while (*is_rfc2047
&& t
.GetNext()) {
120 std::string part
= t
.token();
121 switch (part_index
) {
130 // Do we need charset validity check here?
135 if (part
.size() > 1 ||
136 part
.find_first_of("bBqQ") == std::string::npos
) {
140 if (part
[0] == 'b' || part
[0] == 'B') {
141 enc_type
= B_ENCODING
;
146 *is_rfc2047
= DecodeBQEncoding(part
, enc_type
, charset
, &decoded_word
);
148 // Last minute failure. Invalid B/Q encoding. Rather than
149 // passing it through, return now.
156 // Another last minute failure !
157 // Likely to be a case of two encoded-words in a row or
158 // an encoded word followed by a non-encoded word. We can be
159 // generous, but it does not help much in terms of compatibility,
160 // I believe. Return immediately.
173 if (*(encoded_word
.end() - 1) == '=') {
174 output
->swap(decoded_word
);
175 *parse_result_flags
|=
176 net::HttpContentDisposition::HAS_RFC2047_ENCODED_STRINGS
;
179 // encoded_word ending prematurelly with '?' or extra '?'
184 // We're not handling 'especial' characters quoted with '\', but
185 // it should be Ok because we're not an email client but a
188 // What IE6/7 does: %-escaped UTF-8.
189 decoded_word
= net::UnescapeURLComponent(encoded_word
,
190 net::UnescapeRule::SPACES
);
191 if (decoded_word
!= encoded_word
)
192 *parse_result_flags
|=
193 net::HttpContentDisposition::HAS_PERCENT_ENCODED_STRINGS
;
194 if (base::IsStringUTF8(decoded_word
)) {
195 output
->swap(decoded_word
);
197 // We can try either the OS default charset or 'origin charset' here,
198 // As far as I can tell, IE does not support it. However, I've seen
199 // web servers emit %-escaped string in a legacy encoding (usually
201 // TODO(jungshik) : Test IE further and consider adding a fallback here.
206 // Decodes the value of a 'filename' or 'name' parameter given as |input|. The
207 // value is supposed to be of the form:
209 // value = token | quoted-string
211 // However we currently also allow RFC 2047 encoding and non-ASCII
212 // strings. Non-ASCII strings are interpreted based on |referrer_charset|.
213 bool DecodeFilenameValue(const std::string
& input
,
214 const std::string
& referrer_charset
,
216 int* parse_result_flags
) {
217 int current_parse_result_flags
= 0;
218 std::string decoded_value
;
219 bool is_previous_token_rfc2047
= true;
221 // Tokenize with whitespace characters.
222 base::StringTokenizer
t(input
, " \t\n\r");
223 t
.set_options(base::StringTokenizer::RETURN_DELIMS
);
224 while (t
.GetNext()) {
225 if (t
.token_is_delim()) {
226 // If the previous non-delimeter token is not RFC2047-encoded,
227 // put in a space in its place. Otheriwse, skip over it.
228 if (!is_previous_token_rfc2047
)
229 decoded_value
.push_back(' ');
232 // We don't support a single multibyte character split into
233 // adjacent encoded words. Some broken mail clients emit headers
234 // with that problem, but most web servers usually encode a filename
235 // in a single encoded-word. Firefox/Thunderbird do not support
238 if (!DecodeWord(t
.token(), referrer_charset
, &is_previous_token_rfc2047
,
239 &decoded
, ¤t_parse_result_flags
))
241 decoded_value
.append(decoded
);
243 output
->swap(decoded_value
);
244 if (parse_result_flags
&& !output
->empty())
245 *parse_result_flags
|= current_parse_result_flags
;
249 // Parses the charset and value-chars out of an ext-value string.
251 // ext-value = charset "'" [ language ] "'" value-chars
252 bool ParseExtValueComponents(const std::string
& input
,
253 std::string
* charset
,
254 std::string
* value_chars
) {
255 base::StringTokenizer
t(input
, "'");
256 t
.set_options(base::StringTokenizer::RETURN_DELIMS
);
257 std::string temp_charset
;
258 std::string temp_value
;
259 int numDelimsSeen
= 0;
260 while (t
.GetNext()) {
261 if (t
.token_is_delim()) {
265 switch (numDelimsSeen
) {
267 temp_charset
= t
.token();
270 // Language is ignored.
273 temp_value
= t
.token();
280 if (numDelimsSeen
!= 2)
282 if (temp_charset
.empty() || temp_value
.empty())
284 charset
->swap(temp_charset
);
285 value_chars
->swap(temp_value
);
289 // http://tools.ietf.org/html/rfc5987#section-3.2
291 // ext-value = charset "'" [ language ] "'" value-chars
293 // charset = "UTF-8" / "ISO-8859-1" / mime-charset
295 // mime-charset = 1*mime-charsetc
296 // mime-charsetc = ALPHA / DIGIT
297 // / "!" / "#" / "$" / "%" / "&"
298 // / "+" / "-" / "^" / "_" / "`"
301 // language = <Language-Tag, defined in [RFC5646], Section 2.1>
303 // value-chars = *( pct-encoded / attr-char )
305 // pct-encoded = "%" HEXDIG HEXDIG
307 // attr-char = ALPHA / DIGIT
308 // / "!" / "#" / "$" / "&" / "+" / "-" / "."
309 // / "^" / "_" / "`" / "|" / "~"
310 bool DecodeExtValue(const std::string
& param_value
, std::string
* decoded
) {
311 if (param_value
.find('"') != std::string::npos
)
316 if (!ParseExtValueComponents(param_value
, &charset
, &value
))
319 // RFC 5987 value should be ASCII-only.
320 if (!base::IsStringASCII(value
)) {
325 std::string unescaped
= net::UnescapeURLComponent(
326 value
, net::UnescapeRule::SPACES
| net::UnescapeRule::URL_SPECIAL_CHARS
);
328 return net::ConvertToUtf8AndNormalize(unescaped
, charset
.c_str(), decoded
);
335 HttpContentDisposition::HttpContentDisposition(
336 const std::string
& header
, const std::string
& referrer_charset
)
338 parse_result_flags_(INVALID
) {
339 Parse(header
, referrer_charset
);
342 HttpContentDisposition::~HttpContentDisposition() {
345 std::string::const_iterator
HttpContentDisposition::ConsumeDispositionType(
346 std::string::const_iterator begin
, std::string::const_iterator end
) {
347 DCHECK(type_
== INLINE
);
348 std::string::const_iterator delimiter
= std::find(begin
, end
, ';');
350 std::string::const_iterator type_begin
= begin
;
351 std::string::const_iterator type_end
= delimiter
;
352 HttpUtil::TrimLWS(&type_begin
, &type_end
);
354 // If the disposition-type isn't a valid token the then the
355 // Content-Disposition header is malformed, and we treat the first bytes as
356 // a parameter rather than a disposition-type.
357 if (!HttpUtil::IsToken(type_begin
, type_end
))
360 parse_result_flags_
|= HAS_DISPOSITION_TYPE
;
362 DCHECK(std::find(type_begin
, type_end
, '=') == type_end
);
364 if (LowerCaseEqualsASCII(type_begin
, type_end
, "inline")) {
366 } else if (LowerCaseEqualsASCII(type_begin
, type_end
, "attachment")) {
369 parse_result_flags_
|= HAS_UNKNOWN_DISPOSITION_TYPE
;
375 // http://tools.ietf.org/html/rfc6266
377 // content-disposition = "Content-Disposition" ":"
378 // disposition-type *( ";" disposition-parm )
380 // disposition-type = "inline" | "attachment" | disp-ext-type
381 // ; case-insensitive
382 // disp-ext-type = token
384 // disposition-parm = filename-parm | disp-ext-parm
386 // filename-parm = "filename" "=" value
387 // | "filename*" "=" ext-value
389 // disp-ext-parm = token "=" value
390 // | ext-token "=" ext-value
391 // ext-token = <the characters in token, followed by "*">
393 void HttpContentDisposition::Parse(const std::string
& header
,
394 const std::string
& referrer_charset
) {
395 DCHECK(type_
== INLINE
);
396 DCHECK(filename_
.empty());
398 std::string::const_iterator pos
= header
.begin();
399 std::string::const_iterator end
= header
.end();
400 pos
= ConsumeDispositionType(pos
, end
);
403 std::string filename
;
404 std::string ext_filename
;
406 HttpUtil::NameValuePairsIterator
iter(pos
, end
, ';');
407 while (iter
.GetNext()) {
408 if (filename
.empty() && LowerCaseEqualsASCII(iter
.name_begin(),
411 DecodeFilenameValue(iter
.value(), referrer_charset
, &filename
,
412 &parse_result_flags_
);
413 if (!filename
.empty())
414 parse_result_flags_
|= HAS_FILENAME
;
415 } else if (name
.empty() && LowerCaseEqualsASCII(iter
.name_begin(),
418 DecodeFilenameValue(iter
.value(), referrer_charset
, &name
, NULL
);
420 parse_result_flags_
|= HAS_NAME
;
421 } else if (ext_filename
.empty() && LowerCaseEqualsASCII(iter
.name_begin(),
424 DecodeExtValue(iter
.raw_value(), &ext_filename
);
425 if (!ext_filename
.empty())
426 parse_result_flags_
|= HAS_EXT_FILENAME
;
430 if (!ext_filename
.empty())
431 filename_
= ext_filename
;
432 else if (!filename
.empty())
433 filename_
= filename
;