File: System\Net\Mime\ContentDisposition.cs
Web Access
Project: src\src\libraries\System.Net.Mail\src\System.Net.Mail.csproj (System.Net.Mail)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Mail;
using System.Text;
 
namespace System.Net.Mime
{
    public class ContentDisposition
    {
        private const string CreationDateKey = "creation-date";
        private const string ModificationDateKey = "modification-date";
        private const string ReadDateKey = "read-date";
        private const string FileNameKey = "filename";
        private const string SizeKey = "size";
 
        private TrackingValidationObjectDictionary? _parameters;
        private string _disposition;
        private string _dispositionType;
        private bool _isChanged;
        private bool _isPersisted;
 
        private static readonly TrackingValidationObjectDictionary.ValidateAndParseValue s_dateParser =
                new TrackingValidationObjectDictionary.ValidateAndParseValue(v => new SmtpDateTime(v.ToString()!));
        // this will throw a FormatException if the value supplied is not a valid SmtpDateTime
 
        private static readonly TrackingValidationObjectDictionary.ValidateAndParseValue s_longParser =
                new TrackingValidationObjectDictionary.ValidateAndParseValue((object value) =>
                {
                    long longValue;
                    if (!long.TryParse(value.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out longValue))
                    {
                        throw new FormatException(SR.ContentDispositionInvalid);
                    }
                    return longValue;
                });
 
        private static readonly Dictionary<string, TrackingValidationObjectDictionary.ValidateAndParseValue> s_validators =
            new Dictionary<string, TrackingValidationObjectDictionary.ValidateAndParseValue>() {
                { CreationDateKey, s_dateParser },
                { ModificationDateKey, s_dateParser },
                { ReadDateKey, s_dateParser },
                { SizeKey, s_longParser }
            };
 
        public ContentDisposition()
        {
            _isChanged = true;
            _disposition = _dispositionType = "attachment";
            // no need to parse disposition since there's nothing to parse
        }
 
        public ContentDisposition(string disposition)
        {
            ArgumentNullException.ThrowIfNull(disposition);
 
            _isChanged = true;
            _disposition = disposition;
            ParseValue();
        }
 
        internal DateTime GetDateParameter(string parameterName)
        {
            SmtpDateTime? dateValue = ((TrackingValidationObjectDictionary)Parameters).InternalGet(parameterName) as SmtpDateTime;
            return dateValue == null ? DateTime.MinValue : dateValue.Date;
        }
 
        /// <summary>
        /// Gets the disposition type of the content.
        /// </summary>
        public string DispositionType
        {
            get { return _dispositionType; }
            set
            {
                ArgumentException.ThrowIfNullOrEmpty(value);
 
                _isChanged = true;
                _dispositionType = value;
            }
        }
 
        public StringDictionary Parameters => _parameters ??= new TrackingValidationObjectDictionary(s_validators);
 
        /// <summary>
        /// Gets the value of the Filename parameter.
        /// </summary>
        public string? FileName
        {
            get { return Parameters[FileNameKey]; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    Parameters.Remove(FileNameKey);
                }
                else
                {
                    Parameters[FileNameKey] = value;
                }
            }
        }
 
        /// <summary>
        /// Gets the value of the Creation-Date parameter.
        /// </summary>
        public DateTime CreationDate
        {
            get { return GetDateParameter(CreationDateKey); }
            set
            {
                SmtpDateTime date = new SmtpDateTime(value);
                ((TrackingValidationObjectDictionary)Parameters).InternalSet(CreationDateKey, date);
            }
        }
 
        /// <summary>
        /// Gets the value of the Modification-Date parameter.
        /// </summary>
        public DateTime ModificationDate
        {
            get { return GetDateParameter(ModificationDateKey); }
            set
            {
                SmtpDateTime date = new SmtpDateTime(value);
                ((TrackingValidationObjectDictionary)Parameters).InternalSet(ModificationDateKey, date);
            }
        }
 
        public bool Inline
        {
            get { return _dispositionType == DispositionTypeNames.Inline; }
            set
            {
                _isChanged = true;
                _dispositionType = value ? DispositionTypeNames.Inline : DispositionTypeNames.Attachment;
            }
        }
 
        /// <summary>
        /// Gets the value of the Read-Date parameter.
        /// </summary>
        public DateTime ReadDate
        {
            get { return GetDateParameter(ReadDateKey); }
            set
            {
                SmtpDateTime date = new SmtpDateTime(value);
                ((TrackingValidationObjectDictionary)Parameters).InternalSet(ReadDateKey, date);
            }
        }
 
        /// <summary>
        /// Gets the value of the Size parameter (-1 if unspecified).
        /// </summary>
        public long Size
        {
            get
            {
                object? sizeValue = ((TrackingValidationObjectDictionary)Parameters).InternalGet(SizeKey);
                return sizeValue == null ? -1 : (long)sizeValue;
            }
            set
            {
                ((TrackingValidationObjectDictionary)Parameters).InternalSet(SizeKey, value);
            }
        }
 
        internal void Set(string contentDisposition, HeaderCollection headers)
        {
            // we don't set ischanged because persistence was already handled
            // via the headers.
            _disposition = contentDisposition;
            ParseValue();
            headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition)!, ToString());
            _isPersisted = true;
        }
 
        internal void PersistIfNeeded(HeaderCollection headers, bool forcePersist)
        {
            if (IsChanged || !_isPersisted || forcePersist)
            {
                headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition)!, ToString());
                _isPersisted = true;
            }
        }
 
        internal bool IsChanged => _isChanged || _parameters != null && _parameters.IsChanged;
 
        public override string ToString()
        {
            if (_disposition == null || _isChanged || _parameters != null && _parameters.IsChanged)
            {
                _disposition = Encode(false); // Legacy wire-safe format
                _isChanged = false;
                _parameters!.IsChanged = false;
                _isPersisted = false;
            }
            return _disposition;
        }
 
        internal string Encode(bool allowUnicode)
        {
            var builder = new StringBuilder();
            builder.Append(_dispositionType); // Must not have unicode, already validated
 
            // Validate and encode unicode where required
            foreach (string key in Parameters.Keys)
            {
                builder.Append("; ");
                EncodeToBuffer(key, builder, allowUnicode);
 
                builder.Append('=');
                EncodeToBuffer(_parameters![key]!, builder, allowUnicode);
            }
 
            return builder.ToString();
        }
 
        private static void EncodeToBuffer(string value, StringBuilder builder, bool allowUnicode)
        {
            Encoding? encoding = MimeBasePart.DecodeEncoding(value);
            if (encoding != null) // Manually encoded elsewhere, pass through
            {
                builder.Append('"').Append(value).Append('"');
            }
            else if ((allowUnicode && !MailBnfHelper.HasCROrLF(value)) // Unicode without CL or LF's
                || MimeBasePart.IsAscii(value, false)) // Ascii
            {
                MailBnfHelper.GetTokenOrQuotedString(value, builder, allowUnicode);
            }
            else
            {
                // MIME Encoding required
                encoding = Encoding.GetEncoding(MimeBasePart.DefaultCharSet);
                builder.Append('"').Append(MimeBasePart.EncodeHeaderValue(value, encoding, MimeBasePart.ShouldUseBase64Encoding(encoding))).Append('"');
            }
        }
 
        public override bool Equals([NotNullWhen(true)] object? rparam)
        {
            return rparam == null ?
                false :
                string.Equals(ToString(), rparam.ToString(), StringComparison.OrdinalIgnoreCase);
        }
 
        public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(ToString());
 
        [MemberNotNull(nameof(_dispositionType))]
        private void ParseValue()
        {
            int offset = 0;
            try
            {
                // the disposition MUST be the first parameter in the string
                _dispositionType = MailBnfHelper.ReadToken(_disposition, ref offset);
 
                // disposition MUST not be empty
                if (string.IsNullOrEmpty(_dispositionType))
                {
                    throw new FormatException(SR.MailHeaderFieldMalformedHeader);
                }
 
                // now we know that there are parameters so we must initialize or clear
                // and parse
                if (_parameters == null)
                {
                    _parameters = new TrackingValidationObjectDictionary(s_validators);
                }
                else
                {
                    _parameters.Clear();
                }
 
                while (MailBnfHelper.SkipCFWS(_disposition, ref offset))
                {
                    // ensure that the separator charactor is present
                    if (_disposition[offset++] != ';')
                    {
                        throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, _disposition[offset - 1]));
                    }
 
                    // skip whitespace and see if there's anything left to parse or if we're done
                    if (!MailBnfHelper.SkipCFWS(_disposition, ref offset))
                    {
                        break;
                    }
 
                    string? paramAttribute = MailBnfHelper.ReadParameterAttribute(_disposition, ref offset);
                    string? paramValue;
 
                    // verify the next character after the parameter is correct
                    if (_disposition[offset++] != '=')
                    {
                        throw new FormatException(SR.MailHeaderFieldMalformedHeader);
                    }
 
                    if (!MailBnfHelper.SkipCFWS(_disposition, ref offset))
                    {
                        // parameter was at end of string and has no value
                        // this is not valid
                        throw new FormatException(SR.ContentDispositionInvalid);
                    }
 
                    paramValue = _disposition[offset] == '"' ?
                        MailBnfHelper.ReadQuotedString(_disposition, ref offset, null) :
                        MailBnfHelper.ReadToken(_disposition, ref offset);
 
                    // paramValue could potentially still be empty if it was a valid quoted string that
                    // contained no inner value.  this is invalid
                    if (string.IsNullOrEmpty(paramAttribute) || string.IsNullOrEmpty(paramValue))
                    {
                        throw new FormatException(SR.ContentDispositionInvalid);
                    }
 
                    // if validation is needed, the parameters dictionary will have a validator registered
                    // for the parameter that is being set so no additional formatting checks are needed here
                    Parameters.Add(paramAttribute, paramValue);
                }
            }
            catch (FormatException exception)
            {
                // it's possible that something in MailBNFHelper could throw so ensure that we catch it and wrap it
                // so that the exception has the correct text
                throw new FormatException(SR.ContentDispositionInvalid, exception);
            }
 
            _parameters.IsChanged = false;
        }
    }
}