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