PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/WCFWebApi/src/System.Net.Http/System/Net/Http/Headers/ContentDispositionHeaderValue.cs

#
C# | 616 lines | 487 code | 78 blank | 51 comment | 101 complexity | 59f8b86e8926ed74d6c696d05e7bef89 MD5 | raw file
Possible License(s): CC-BY-SA-3.0, Apache-2.0
  1. using System.Collections.Generic;
  2. using System.Diagnostics.Contracts;
  3. using System.Globalization;
  4. using System.Net.Mime;
  5. using System.Text;
  6. namespace System.Net.Http.Headers
  7. {
  8. public class ContentDispositionHeaderValue : ICloneable
  9. {
  10. #region Fields
  11. private const string fileName = "filename";
  12. private const string name = "name";
  13. private const string fileNameStar = "filename*";
  14. private const string creationDate = "creation-date";
  15. private const string modificationDate = "modification-date";
  16. private const string readDate = "read-date";
  17. private const string size = "size";
  18. // Use list instead of dictionary since we may have multiple parameters with the same name.
  19. private ICollection<NameValueHeaderValue> parameters;
  20. private string dispositionType;
  21. #endregion Fields
  22. #region Properties
  23. public string DispositionType
  24. {
  25. get { return dispositionType; }
  26. set
  27. {
  28. CheckDispositionTypeFormat(value, "value");
  29. dispositionType = value;
  30. }
  31. }
  32. public ICollection<NameValueHeaderValue> Parameters
  33. {
  34. get
  35. {
  36. if (parameters == null)
  37. {
  38. parameters = new ObjectCollection<NameValueHeaderValue>();
  39. }
  40. return parameters;
  41. }
  42. }
  43. // Helpers to access specific parameters in the list
  44. public string Name
  45. {
  46. get { return GetName(name); }
  47. set { SetName(name, value); }
  48. }
  49. public string FileName
  50. {
  51. get { return GetName(fileName); }
  52. set { SetName(fileName, value); }
  53. }
  54. public string FileNameStar
  55. {
  56. get { return GetName(fileNameStar); }
  57. set { SetName(fileNameStar, value); }
  58. }
  59. public DateTimeOffset? CreationDate
  60. {
  61. get { return GetDate(creationDate); }
  62. set { SetDate(creationDate, value); }
  63. }
  64. public DateTimeOffset? ModificationDate
  65. {
  66. get { return GetDate(modificationDate); }
  67. set { SetDate(modificationDate, value); }
  68. }
  69. public DateTimeOffset? ReadDate
  70. {
  71. get { return GetDate(readDate); }
  72. set { SetDate(readDate, value); }
  73. }
  74. public long? Size
  75. {
  76. get
  77. {
  78. NameValueHeaderValue sizeParameter = NameValueHeaderValue.Find(parameters, size);
  79. ulong value;
  80. if (sizeParameter != null)
  81. {
  82. string sizeString = sizeParameter.Value;
  83. if (UInt64.TryParse(sizeString, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
  84. {
  85. return (long)value;
  86. }
  87. }
  88. return null;
  89. }
  90. set
  91. {
  92. NameValueHeaderValue sizeParameter = NameValueHeaderValue.Find(parameters, size);
  93. if (value == null)
  94. {
  95. // Remove parameter
  96. if (sizeParameter != null)
  97. {
  98. parameters.Remove(sizeParameter);
  99. }
  100. }
  101. else if (value < 0)
  102. {
  103. throw new ArgumentOutOfRangeException("value");
  104. }
  105. else if (sizeParameter != null)
  106. {
  107. sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
  108. }
  109. else
  110. {
  111. string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
  112. parameters.Add(new NameValueHeaderValue(size, sizeString));
  113. }
  114. }
  115. }
  116. #endregion Properties
  117. #region Constructors
  118. internal ContentDispositionHeaderValue()
  119. {
  120. // Used by the parser to create a new instance of this type.
  121. }
  122. protected ContentDispositionHeaderValue(ContentDispositionHeaderValue source)
  123. {
  124. Contract.Requires(source != null);
  125. this.dispositionType = source.dispositionType;
  126. if (source.parameters != null)
  127. {
  128. foreach (var parameter in source.parameters)
  129. {
  130. this.Parameters.Add((NameValueHeaderValue)((ICloneable)parameter).Clone());
  131. }
  132. }
  133. }
  134. public ContentDispositionHeaderValue(string dispositionType)
  135. {
  136. CheckDispositionTypeFormat(dispositionType, "dispositionType");
  137. this.dispositionType = dispositionType;
  138. }
  139. #endregion Constructors
  140. #region Overloads
  141. public override string ToString()
  142. {
  143. return dispositionType + NameValueHeaderValue.ToString(parameters, ';', true);
  144. }
  145. public override bool Equals(object obj)
  146. {
  147. ContentDispositionHeaderValue other = obj as ContentDispositionHeaderValue;
  148. if (other == null)
  149. {
  150. return false;
  151. }
  152. return (string.Compare(dispositionType, other.dispositionType, StringComparison.OrdinalIgnoreCase) == 0) &&
  153. HeaderUtilities.AreEqualCollections(parameters, other.parameters);
  154. }
  155. public override int GetHashCode()
  156. {
  157. // The dispositionType string is case-insensitive.
  158. return dispositionType.ToLowerInvariant().GetHashCode() ^ NameValueHeaderValue.GetHashCode(parameters);
  159. }
  160. // Implement ICloneable explicitly to allow derived types to "override" the implementation.
  161. object ICloneable.Clone()
  162. {
  163. return new ContentDispositionHeaderValue(this);
  164. }
  165. #endregion Overloads
  166. #region Parsing
  167. public static ContentDispositionHeaderValue Parse(string input)
  168. {
  169. int index = 0;
  170. return (ContentDispositionHeaderValue)GenericHeaderParser.ContentDispositionParser.ParseValue(input,
  171. null, ref index);
  172. }
  173. public static bool TryParse(string input, out ContentDispositionHeaderValue parsedValue)
  174. {
  175. int index = 0;
  176. object output;
  177. parsedValue = null;
  178. if (GenericHeaderParser.ContentDispositionParser.TryParseValue(input, null, ref index, out output))
  179. {
  180. parsedValue = (ContentDispositionHeaderValue)output;
  181. return true;
  182. }
  183. return false;
  184. }
  185. internal static int GetDispositionTypeLength(string input, int startIndex, out object parsedValue)
  186. {
  187. Contract.Requires(startIndex >= 0);
  188. parsedValue = null;
  189. if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
  190. {
  191. return 0;
  192. }
  193. // Caller must remove leading whitespaces. If not, we'll return 0.
  194. string dispositionType = null;
  195. int dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out dispositionType);
  196. if (dispositionTypeLength == 0)
  197. {
  198. return 0;
  199. }
  200. int current = startIndex + dispositionTypeLength;
  201. current = current + HttpRuleParser.GetWhitespaceLength(input, current);
  202. ContentDispositionHeaderValue contentDispositionHeader = new ContentDispositionHeaderValue();
  203. contentDispositionHeader.dispositionType = dispositionType;
  204. // If we're not done and we have a parameter delimiter, then we have a list of parameters.
  205. if ((current < input.Length) && (input[current] == ';'))
  206. {
  207. current++; // skip delimiter.
  208. int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
  209. contentDispositionHeader.Parameters);
  210. if (parameterLength == 0)
  211. {
  212. return 0;
  213. }
  214. parsedValue = contentDispositionHeader;
  215. return current + parameterLength - startIndex;
  216. }
  217. // We have a ContentDisposition header without parameters.
  218. parsedValue = contentDispositionHeader;
  219. return current - startIndex;
  220. }
  221. private static int GetDispositionTypeExpressionLength(string input, int startIndex, out string dispositionType)
  222. {
  223. Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length));
  224. // This method just parses the disposition type string, it does not parse parameters.
  225. dispositionType = null;
  226. // Parse the disposition type, i.e. <dispositiontype> in content-disposition string
  227. // "<dispositiontype>; param1=value1; param2=value2"
  228. int typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
  229. if (typeLength == 0)
  230. {
  231. return 0;
  232. }
  233. dispositionType = input.Substring(startIndex, typeLength);
  234. return typeLength;
  235. }
  236. private static void CheckDispositionTypeFormat(string dispositionType, string parameterName)
  237. {
  238. if (string.IsNullOrEmpty(dispositionType))
  239. {
  240. throw new ArgumentException(SR.net_http_argument_empty_string, parameterName);
  241. }
  242. // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
  243. string tempDispositionType;
  244. int dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out tempDispositionType);
  245. if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length))
  246. {
  247. throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture,
  248. SR.net_http_headers_invalid_value, dispositionType));
  249. }
  250. }
  251. #endregion Parsing
  252. #region Helpers
  253. // Gets a paramiter of the given name and attempts to extract a date.
  254. // Returns null if the parameter is not present or the format is incorrect.
  255. private DateTimeOffset? GetDate(string parameter)
  256. {
  257. NameValueHeaderValue dateParameter = NameValueHeaderValue.Find(parameters, parameter);
  258. DateTimeOffset date;
  259. if (dateParameter != null)
  260. {
  261. string dateString = dateParameter.Value;
  262. // Should have quotes, remove them.
  263. if (IsQuoted(dateString))
  264. {
  265. dateString = dateString.Substring(1, dateString.Length - 2);
  266. }
  267. if (HttpRuleParser.TryStringToDate(dateString, out date))
  268. {
  269. return date;
  270. }
  271. }
  272. return null;
  273. }
  274. // Add the given parameter to the list. Remove if date is null.
  275. private void SetDate(string parameter, DateTimeOffset? date)
  276. {
  277. NameValueHeaderValue dateParameter = NameValueHeaderValue.Find(parameters, parameter);
  278. if (date == null)
  279. {
  280. // Remove parameter
  281. if (dateParameter != null)
  282. {
  283. parameters.Remove(dateParameter);
  284. }
  285. }
  286. else
  287. {
  288. // Must always be quoted
  289. string dateString = string.Format(CultureInfo.InvariantCulture, "\"{0}\"",
  290. HttpRuleParser.DateToString(date.Value));
  291. if (dateParameter != null)
  292. {
  293. dateParameter.Value = dateString;
  294. }
  295. else
  296. {
  297. Parameters.Add(new NameValueHeaderValue(parameter, dateString));
  298. }
  299. }
  300. }
  301. // Gets a parameter of the given name and attempts to decode it if nessisary.
  302. // Returns null if the parameter is not present or the raw value if the encoding is incorrect.
  303. private string GetName(string parameter)
  304. {
  305. NameValueHeaderValue nameParameter = NameValueHeaderValue.Find(parameters, parameter);
  306. if (nameParameter != null)
  307. {
  308. string result;
  309. // filename*=utf-8'lang'%7FMyString
  310. if (parameter.EndsWith("*", StringComparison.Ordinal))
  311. {
  312. if (TryDecode5987(nameParameter.Value, out result))
  313. {
  314. return result;
  315. }
  316. return null; // Unrecognized encoding
  317. }
  318. // filename="=?utf-8?B?BDFSDFasdfasdc==?="
  319. if (TryDecodeMime(nameParameter.Value, out result))
  320. {
  321. return result;
  322. }
  323. // May not have been encoded
  324. return nameParameter.Value;
  325. }
  326. return null;
  327. }
  328. // Add/update the given parameter in the list, encoding if nessisary.
  329. // Remove if value is null/Empty
  330. private void SetName(string parameter, string value)
  331. {
  332. NameValueHeaderValue nameParameter = NameValueHeaderValue.Find(parameters, parameter);
  333. if (string.IsNullOrEmpty(value))
  334. {
  335. // Remove parameter
  336. if (nameParameter != null)
  337. {
  338. parameters.Remove(nameParameter);
  339. }
  340. }
  341. else
  342. {
  343. string processedValue = string.Empty;
  344. if (parameter.EndsWith("*", StringComparison.Ordinal))
  345. {
  346. processedValue = Encode5987(value);
  347. }
  348. else
  349. {
  350. processedValue = EncodeAndQuoteMime(value);
  351. }
  352. if (nameParameter != null)
  353. {
  354. nameParameter.Value = processedValue;
  355. }
  356. else
  357. {
  358. Parameters.Add(new NameValueHeaderValue(parameter, processedValue));
  359. }
  360. }
  361. }
  362. // Returns input for decoding failures, as the content might not be encoded
  363. private string EncodeAndQuoteMime(string input)
  364. {
  365. string result = input;
  366. bool needsQuotes = false;
  367. // Remove bounding quotes, they'll get re-added later
  368. if (IsQuoted(result))
  369. {
  370. result = result.Substring(1, result.Length - 2);
  371. needsQuotes = true;
  372. }
  373. if (result.IndexOf("\"", 0, StringComparison.Ordinal) >= 0) // Only bounding quotes are allowed
  374. {
  375. throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
  376. SR.net_http_headers_invalid_value, input));
  377. }
  378. else if (RequiresEncoding(result))
  379. {
  380. needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens
  381. result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?=
  382. }
  383. else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length)
  384. {
  385. needsQuotes = true;
  386. }
  387. if (needsQuotes)
  388. {
  389. // Re-add quotes "value"
  390. result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result);
  391. }
  392. return result;
  393. }
  394. // Returns true if the value starts and ends with a quote
  395. private bool IsQuoted(string value)
  396. {
  397. Contract.Assert(value != null);
  398. return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal)
  399. && value.EndsWith("\"", StringComparison.Ordinal);
  400. }
  401. // tspecials are required to be in a quoted string. Only non-ascii needs to be encoded.
  402. private bool RequiresEncoding(string input)
  403. {
  404. Contract.Assert(input != null);
  405. foreach (char c in input)
  406. {
  407. if ((int)c > 0x7f)
  408. {
  409. return true;
  410. }
  411. }
  412. return false;
  413. }
  414. // Encode using MIME encoding
  415. private string EncodeMime(string input)
  416. {
  417. byte[] buffer = Encoding.UTF8.GetBytes(input);
  418. string encodedName = Convert.ToBase64String(buffer);
  419. return string.Format(CultureInfo.InvariantCulture, "=?utf-8?B?{0}?=", encodedName);
  420. }
  421. // Attempt to decode MIME encoded strings
  422. private bool TryDecodeMime(string input, out string output)
  423. {
  424. Contract.Assert(input != null);
  425. output = null;
  426. string processedInput = input;
  427. // Require quotes, min of "=?e?b??="
  428. if (!IsQuoted(processedInput) || processedInput.Length < 10)
  429. {
  430. return false;
  431. }
  432. string[] parts = processedInput.Split('?');
  433. // "=, encodingName, encodingType, encodedData, ="
  434. if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" || parts[2].ToLowerInvariant() != "b")
  435. {
  436. // Not encoded.
  437. // This does not support multi-line encoding.
  438. // Only base64 encoding is supported, not quoted printable
  439. return false;
  440. }
  441. try
  442. {
  443. Encoding encoding = Encoding.GetEncoding(parts[1]);
  444. byte[] bytes = Convert.FromBase64String(parts[3]);
  445. output = encoding.GetString(bytes);
  446. return true;
  447. }
  448. catch (ArgumentException)
  449. {
  450. // Unknown encoding or bad characters
  451. }
  452. catch (FormatException)
  453. {
  454. // Bad base64 decoding
  455. }
  456. return false;
  457. }
  458. // Encode a string using RFC 5987 encoding
  459. // encoding'lang'PercentEncodedSpecials
  460. private string Encode5987(string input)
  461. {
  462. StringBuilder builder = new StringBuilder("utf-8\'\'");
  463. foreach (char c in input)
  464. {
  465. // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
  466. // ; token except ( "*" / "'" / "%" )
  467. if (c > 0x7F) // Encodes as multiple utf-8 bytes
  468. {
  469. byte[] bytes = Encoding.UTF8.GetBytes(c.ToString());
  470. foreach (byte b in bytes)
  471. {
  472. builder.Append(Uri.HexEscape((char)b));
  473. }
  474. }
  475. else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%')
  476. {
  477. // ASCII - Only one encoded byte
  478. builder.Append(Uri.HexEscape(c));
  479. }
  480. else
  481. {
  482. builder.Append(c);
  483. }
  484. }
  485. return builder.ToString();
  486. }
  487. // Attempt to decode using RFC 5987 encoding.
  488. // encoding'language'my%20string
  489. private bool TryDecode5987(string input, out string output)
  490. {
  491. output = null;
  492. string[] parts = input.Split('\'');
  493. if (parts.Length != 3)
  494. {
  495. return false;
  496. }
  497. StringBuilder decoded = new StringBuilder();
  498. try
  499. {
  500. Encoding encoding = Encoding.GetEncoding(parts[0]);
  501. string dataString = parts[2];
  502. byte[] unescapedBytes = new byte[dataString.Length];
  503. int unescapedBytesCount = 0;
  504. for (int index = 0; index < dataString.Length; index++)
  505. {
  506. if (Uri.IsHexEncoding(dataString, index)) // %FF
  507. {
  508. // Unescape and cache bytes, multi-byte characters must be decoded all at once
  509. unescapedBytes[unescapedBytesCount++] = (byte)Uri.HexUnescape(dataString, ref index);
  510. index--; // HexUnescape did +=3; Offset the for loop's ++
  511. }
  512. else
  513. {
  514. if (unescapedBytesCount > 0)
  515. {
  516. // Decode any previously cached bytes
  517. decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
  518. unescapedBytesCount = 0;
  519. }
  520. decoded.Append(dataString[index]); // Normal safe character
  521. }
  522. }
  523. if (unescapedBytesCount > 0)
  524. {
  525. // Decode any previously cached bytes
  526. decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
  527. }
  528. }
  529. catch (ArgumentException)
  530. {
  531. return false; // Unknown encoding or bad characters
  532. }
  533. output = decoded.ToString();
  534. return true;
  535. }
  536. #endregion Helpers
  537. }
  538. }