diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs index 588e13db40..1bb82eec15 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/OutputFormatterCanWriteContext.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.Formatters public abstract class OutputFormatterCanWriteContext { /// - /// Gets or sets the of the content type to write to the response. + /// Gets or sets the content type to write to the response. /// /// /// An can set this value when its diff --git a/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs index 7b44014b56..f2ce65cb80 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs @@ -68,14 +68,27 @@ namespace Microsoft.AspNet.Mvc // Confirm the request's content type is more specific than a media type this action supports e.g. OK // if client sent "text/plain" data and this action supports "text/*". - if (requestContentType != null && - !ContentTypes.Any(contentType => MediaTypeComparisons.IsSubsetOf(contentType, requestContentType))) + if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType)) { context.Result = new UnsupportedMediaTypeResult(); } } } + private bool IsSubsetOfAnyContentType(string requestMediaType) + { + var parsedRequestMediaType = new MediaType(requestMediaType); + for (var i = 0; i < ContentTypes.Count; i++) + { + var contentTypeMediaType = new MediaType(ContentTypes[i]); + if (parsedRequestMediaType.IsSubsetOf(contentTypeMediaType)) + { + return true; + } + } + return false; + } + /// public void OnResourceExecuted(ResourceExecutedContext context) { @@ -111,9 +124,9 @@ namespace Microsoft.AspNet.Mvc return !isActionWithoutConsumeConstraintPresent; } - // Confirm the request's content type is more specific than a media type this action supports e.g. OK + // Confirm the request's content type is more specific than (a media type this action supports e.g. OK // if client sent "text/plain" data and this action supports "text/*". - if (ContentTypes.Any(contentType => MediaTypeComparisons.IsSubsetOf(contentType, requestContentType))) + if (IsSubsetOfAnyContentType(requestContentType)) { return true; } @@ -181,8 +194,9 @@ namespace Microsoft.AspNet.Mvc var contentTypes = new MediaTypeCollection(); foreach (var arg in completeArgs) { - if (MediaTypeComparisons.MatchesAllSubtypes(arg) || - MediaTypeComparisons.MatchesAllTypes(arg)) + var mediaType = new MediaType(arg); + if (mediaType.MatchesAllSubTypes || + mediaType.MatchesAllTypes) { throw new InvalidOperationException( Resources.FormatMatchAllContentTypeIsNotAllowed(arg)); diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/AcceptHeaderParser.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/AcceptHeaderParser.cs new file mode 100644 index 0000000000..bda6fc6988 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/AcceptHeaderParser.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNet.Mvc.Core; + +namespace Microsoft.AspNet.Mvc.Formatters +{ + public static class AcceptHeaderParser + { + public static IList ParseAcceptHeader(IList acceptHeaders) + { + var parsedValues = new List(); + ParseAcceptHeader(acceptHeaders, parsedValues); + + return parsedValues; + } + + public static void ParseAcceptHeader(IList acceptHeaders, IList parsedValues) + { + if (acceptHeaders == null) + { + throw new ArgumentNullException(nameof(acceptHeaders)); + } + + if (parsedValues == null) + { + throw new ArgumentNullException(nameof(parsedValues)); + } + + for (var i = 0; i < acceptHeaders.Count; i++) + { + var charIndex = 0; + var value = acceptHeaders[i]; + + while (!string.IsNullOrEmpty(value) && charIndex < value.Length) + { + MediaTypeSegmentWithQuality output; + if (TryParseValue(value, ref charIndex, out output)) + { + // The entry may not contain an actual value, like Accept: application/json, , */* + if (output.MediaType.HasValue) + { + parsedValues.Add(output); + } + } + else + { + var invalidValuesError = Resources.FormatAcceptHeaderParser_ParseAcceptHeader_InvalidValues( + value.Substring(charIndex)); + + throw new FormatException(invalidValuesError); + } + } + } + } + + private static bool TryParseValue(string value, ref int index, out MediaTypeSegmentWithQuality parsedValue) + { + parsedValue = default(MediaTypeSegmentWithQuality); + + // The accept header may be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + // In this case, do not fail parsing in case one of the values is the empty string. + if (string.IsNullOrEmpty(value) || (index == value.Length)) + { + return true; + } + + var separatorFound = false; + var currentIndex = GetNextNonEmptyOrWhitespaceIndex(value, index, out separatorFound); + + if (currentIndex == value.Length) + { + index = currentIndex; + return true; + } + + MediaTypeSegmentWithQuality result; + var length = GetMediaTypeWithQualityLength(value, currentIndex, out result); + + if (length == 0) + { + return false; + } + + currentIndex = currentIndex + length; + currentIndex = GetNextNonEmptyOrWhitespaceIndex(value, currentIndex, out separatorFound); + + // If we've not reached the end of the string, then we must have a separator. + // E. g application/json, text/plain <- We must be at ',' otherwise, we've failed parsing. + if (!separatorFound && (currentIndex < value.Length)) + { + return false; + } + + index = currentIndex; + parsedValue = result; + return true; + } + + private static int GetNextNonEmptyOrWhitespaceIndex( + string input, + int startIndex, + out bool separatorFound) + { + Debug.Assert(input != null); + Debug.Assert(startIndex <= input.Length); // it's OK if index == value.Length. + + separatorFound = false; + var current = startIndex + HttpTokenParsingRules.GetWhitespaceLength(input, startIndex); + + if ((current == input.Length) || (input[current] != ',')) + { + return current; + } + + // If we have a separator, skip the separator and all following whitespaces, and + // continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + while ((current < input.Length) && (input[current] == ',')) + { + current++; // skip delimiter. + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + } + + return current; + } + + private static int GetMediaTypeWithQualityLength( + string input, + int start, + out MediaTypeSegmentWithQuality result) + { + result = MediaType.CreateMediaTypeSegmentWithQuality(input, start); + if (result.MediaType.HasValue) + { + return result.MediaType.Length; + } + else + { + return 0; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/FormatFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/FormatFilter.cs index b64bc64d9f..199aa55fc1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/FormatFilter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/FormatFilter.cs @@ -89,13 +89,28 @@ namespace Microsoft.AspNet.Mvc.Formatters // request's format and IApiResponseMetadataProvider-provided content types similarly to an Accept // header and an output formatter's SupportedMediaTypes: Confirm action supports a more specific media // type than requested e.g. OK if "text/*" requested and action supports "text/plain". - if (!supportedMediaTypes.Any(c => MediaTypeComparisons.IsSubsetOf(contentType, c))) + if (!IsSuperSetOfAnySupportedMediaType(contentType, supportedMediaTypes)) { context.Result = new HttpNotFoundResult(); } } } + private bool IsSuperSetOfAnySupportedMediaType(string contentType, MediaTypeCollection supportedMediaTypes) + { + var parsedContentType = new MediaType(contentType); + for (var i = 0; i < supportedMediaTypes.Count; i++) + { + var supportedMediaType = new MediaType(supportedMediaTypes[i]); + if (supportedMediaType.IsSubsetOf(parsedContentType)) + { + return true; + } + } + + return false; + } + /// public void OnResourceExecuted(ResourceExecutedContext context) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpParseResult.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpParseResult.cs new file mode 100644 index 0000000000..86a4fec97c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpParseResult.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc.Formatters +{ + internal enum HttpParseResult + { + Parsed, + NotParsed, + InvalidFormat, + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpTokenParsingRules.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpTokenParsingRules.cs new file mode 100644 index 0000000000..10f9ec0844 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpTokenParsingRules.cs @@ -0,0 +1,273 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; + +namespace Microsoft.AspNet.Mvc.Formatters +{ + internal static class HttpTokenParsingRules + { + private static readonly bool[] TokenChars; + private const int MaxNestedCount = 5; + + internal const char CR = '\r'; + internal const char LF = '\n'; + internal const char SP = ' '; + internal const char Tab = '\t'; + internal const int MaxInt64Digits = 19; + internal const int MaxInt32Digits = 10; + + // iso-8859-1, Western European (ISO) + internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591); + + static HttpTokenParsingRules() + { + // token = 1* + // CTL = + + TokenChars = new bool[128]; // everything is false + + for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127) + { + TokenChars[i] = true; + } + + // remove separators: these are not valid token characters + TokenChars[(byte)'('] = false; + TokenChars[(byte)')'] = false; + TokenChars[(byte)'<'] = false; + TokenChars[(byte)'>'] = false; + TokenChars[(byte)'@'] = false; + TokenChars[(byte)','] = false; + TokenChars[(byte)';'] = false; + TokenChars[(byte)':'] = false; + TokenChars[(byte)'\\'] = false; + TokenChars[(byte)'"'] = false; + TokenChars[(byte)'/'] = false; + TokenChars[(byte)'['] = false; + TokenChars[(byte)']'] = false; + TokenChars[(byte)'?'] = false; + TokenChars[(byte)'='] = false; + TokenChars[(byte)'{'] = false; + TokenChars[(byte)'}'] = false; + } + + internal static bool IsTokenChar(char character) + { + // Must be between 'space' (32) and 'DEL' (127) + if (character > 127) + { + return false; + } + + return TokenChars[character]; + } + + internal static int GetTokenLength(string input, int startIndex) + { + Contract.Requires(input != null); + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + while (current < input.Length) + { + if (!IsTokenChar(input[current])) + { + return current - startIndex; + } + current++; + } + return input.Length - startIndex; + } + + internal static int GetWhitespaceLength(string input, int startIndex) + { + Contract.Requires(input != null); + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + + if (startIndex >= input.Length) + { + return 0; + } + + var current = startIndex; + + char c; + while (current < input.Length) + { + c = input[current]; + + if ((c == SP) || (c == Tab)) + { + current++; + continue; + } + + if (c == CR) + { + // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. + if ((current + 2 < input.Length) && (input[current + 1] == LF)) + { + char spaceOrTab = input[current + 2]; + if ((spaceOrTab == SP) || (spaceOrTab == Tab)) + { + current += 3; + continue; + } + } + } + + return current - startIndex; + } + + // All characters between startIndex and the end of the string are LWS characters. + return input.Length - startIndex; + } + + internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) + { + var nestedCount = 0; + return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); + } + + // quoted-pair = "\" CHAR + // CHAR = + internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) && + (Contract.ValueAtReturn(out length) <= (input.Length - startIndex))); + + length = 0; + + if (input[startIndex] != '\\') + { + return HttpParseResult.NotParsed; + } + + // Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char) + // If so, check whether the character is in the range 0-127. If not, it's an invalid value. + if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) + { + return HttpParseResult.InvalidFormat; + } + + // We don't care what the char next to '\' is. + length = 2; + return HttpParseResult.Parsed; + } + + // TEXT = + // LWS = [CRLF] 1*( SP | HT ) + // CTL = + // + // Since we don't really care about the content of a quoted string or comment, we're more tolerant and + // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). + // + // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like + // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested + // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) + // is unusual. + private static HttpParseResult GetExpressionLength( + string input, + int startIndex, + char openChar, + char closeChar, + bool supportsNesting, + ref int nestedCount, + out int length) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.Result() != HttpParseResult.Parsed) || + (Contract.ValueAtReturn(out length) > 0)); + + length = 0; + + if (input[startIndex] != openChar) + { + return HttpParseResult.NotParsed; + } + + var current = startIndex + 1; // Start parsing with the character next to the first open-char + while (current < input.Length) + { + // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. + // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. + var quotedPairLength = 0; + if ((current + 2 < input.Length) && + (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed)) + { + // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, + // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only + // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). + current = current + quotedPairLength; + continue; + } + + // If we support nested expressions and we find an open-char, then parse the nested expressions. + if (supportsNesting && (input[current] == openChar)) + { + nestedCount++; + try + { + // Check if we exceeded the number of nested calls. + if (nestedCount > MaxNestedCount) + { + return HttpParseResult.InvalidFormat; + } + + var nestedLength = 0; + HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar, + supportsNesting, ref nestedCount, out nestedLength); + + switch (nestedResult) + { + case HttpParseResult.Parsed: + current += nestedLength; // add the length of the nested expression and continue. + break; + + case HttpParseResult.NotParsed: + Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " + + "parsing, because we found the open-char. So either it's a valid nested " + + "expression or it has invalid format."); + break; + + case HttpParseResult.InvalidFormat: + // If the nested expression is invalid, we can't continue, so we fail with invalid format. + return HttpParseResult.InvalidFormat; + + default: + Contract.Assert(false, "Unknown enum result: " + nestedResult); + break; + } + } + finally + { + nestedCount--; + } + } + + if (input[current] == closeChar) + { + length = current - startIndex + 1; + return HttpParseResult.Parsed; + } + current++; + } + + // We didn't see the final quote, therefore we have an invalid expression string. + return HttpParseResult.InvalidFormat; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs index 7330c0ef9d..751ff68729 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs @@ -69,10 +69,21 @@ namespace Microsoft.AspNet.Mvc.Formatters // Confirm the request's content type is more specific than a media type this formatter supports e.g. OK if // client sent "text/plain" data and this formatter supports "text/*". - return SupportedMediaTypes.Any(supportedMediaType => + return IsSubsetOfAnySupportedContentType(contentType); + } + + private bool IsSubsetOfAnySupportedContentType(string contentType) + { + var parsedContentType = new MediaType(contentType); + for (var i = 0; i < SupportedMediaTypes.Count; i++) { - return MediaTypeComparisons.IsSubsetOf(supportedMediaType, contentType); - }); + var supportedMediaType = new MediaType(SupportedMediaTypes[i]); + if (parsedContentType.IsSubsetOf(supportedMediaType)) + { + return true; + } + } + return false; } /// @@ -120,7 +131,7 @@ namespace Microsoft.AspNet.Mvc.Formatters if (request.ContentType != null) { - var encoding = MediaTypeEncoding.GetEncoding(request.ContentType); + var encoding = MediaType.GetEncoding(request.ContentType); if (encoding != null) { foreach (var supportedEncoding in SupportedEncodings) diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaType.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaType.cs new file mode 100644 index 0000000000..3bf928958d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaType.cs @@ -0,0 +1,551 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNet.Mvc.Formatters +{ + /// + /// A media type value. + /// + public struct MediaType + { + private static readonly StringSegment QualityParameter = new StringSegment("q"); + + private MediaTypeParameterParser _parameterParser; + + /// + /// Initializes a instance. + /// + /// The with the media type. + public MediaType(string mediaType) + : this(mediaType, 0, mediaType.Length) + { + } + + /// + /// Initializes a instance. + /// + /// The with the media type. + public MediaType(StringSegment mediaType) + : this(mediaType.Buffer, mediaType.Offset, mediaType.Length) + { + } + + /// + /// Initializes a instance. + /// + /// The with the media type. + /// The offset in the where the parsing starts. + /// The of the media type to parse if provided. + public MediaType(string mediaType, int offset, int? length) + { + if (mediaType == null) + { + throw new ArgumentNullException(nameof(mediaType)); + } + + if (offset < 0 || offset >= mediaType.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length != null && offset + length > mediaType.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _parameterParser = default(MediaTypeParameterParser); + + StringSegment type; + var typeLength = GetTypeLength(mediaType, offset, out type); + if (typeLength == 0) + { + Type = new StringSegment(); + SubType = new StringSegment(); + return; + } + else + { + Type = type; + } + + StringSegment subType; + var subTypeLength = GetSubtypeLength(mediaType, offset + typeLength, out subType); + if (subTypeLength == 0) + { + SubType = new StringSegment(); + return; + } + else + { + SubType = subType; + } + + _parameterParser = new MediaTypeParameterParser(mediaType, offset + typeLength + subTypeLength, length); + } + + // All GetXXXLength methods work in the same way. They expect to be on the right position for + // the token they are parsing, for example, the beginning of the media type or the delimiter + // from a previous token, like '/', ';' or '='. + // Each method consumes the delimiter token if any, the leading whitespace, then the given token + // itself, and finally the trailing whitespace. + private static int GetTypeLength(string input, int offset, out StringSegment type) + { + if (offset < 0 || offset >= input.Length) + { + type = default(StringSegment); + return 0; + } + + // Parse the type, i.e. in media type string "/; param1=value1; param2=value2" + var typeLength = HttpTokenParsingRules.GetTokenLength(input, offset); + + if (typeLength == 0) + { + type = default(StringSegment); + return 0; + } + + type = new StringSegment(input, offset, typeLength); + + var current = offset + typeLength; + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - offset; + } + + private static int GetSubtypeLength(string input, int offset, out StringSegment subType) + { + var current = offset; + // Parse the separator between type and subtype + if (current < 0 || current >= input.Length || input[current] != '/') + { + subType = default(StringSegment); + return 0; + } + + current++; // skip delimiter. + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var subtypeLength = HttpTokenParsingRules.GetTokenLength(input, current); + + if (subtypeLength == 0) + { + subType = default(StringSegment); + return 0; + } + + subType = new StringSegment(input, current, subtypeLength); + + current = current + subtypeLength; + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - offset; + } + + /// + /// Gets the type of the . + /// + public StringSegment Type { get; } + + /// + /// Gets whether this matches all types. + /// + public bool MatchesAllTypes => Type.Equals("*", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the subtype of the . + /// + public StringSegment SubType { get; private set; } + + /// + /// Gets whether this matches all subtypes. + /// + public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the of the if it has one. + /// + public Encoding Encoding => GetEncodingFromCharset(GetParameter("charset")); + + /// + /// Gets the charset parameter of the if it has one. + /// + public StringSegment Charset => GetParameter("charset"); + + /// + /// Determines whether the current is a subset of the . + /// + /// The set . + /// + /// true if this is a subset of ; otherwisefalse. + /// + public bool IsSubsetOf(MediaType set) + { + return (set.MatchesAllTypes || set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase)) && + (set.MatchesAllSubTypes || set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase)) && + ContainsAllParameters(set._parameterParser); + } + + /// + /// Gets the parameter of the media type. + /// + /// The name of the parameter to retrieve. + /// The for the given if found; otherwisenull. + public StringSegment GetParameter(string parameterName) + { + return GetParameter(new StringSegment(parameterName)); + } + + /// + /// Gets the parameter of the media type. + /// + /// The name of the parameter to retrieve. + /// The for the given if found; otherwisenull. + public StringSegment GetParameter(StringSegment parameterName) + { + var parametersParser = _parameterParser; + + MediaTypeParameter parameter; + while (parametersParser.ParseNextParameter(out parameter)) + { + if (parameter.HasName(parameterName)) + { + return parameter.Value; + } + } + + return new StringSegment(); + } + + /// + /// Replaces the encoding of the given with the provided + /// . + /// + /// The media type whose encoding will be replaced. + /// The encoding that will replace the encoding in the + /// A media type with the replaced encoding. + public static string ReplaceEncoding(string mediaType, Encoding encoding) + { + return ReplaceEncoding(new StringSegment(mediaType), encoding); + } + + /// + /// Replaces the encoding of the given with the provided + /// . + /// + /// The media type whose encoding will be replaced. + /// The encoding that will replace the encoding in the + /// A media type with the replaced encoding. + public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding) + { + var parsedMediaType = new MediaType(mediaType); + var charset = parsedMediaType.GetParameter("charset"); + + if (charset.HasValue && + charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase)) + { + return mediaType.Value; + } + + if (!charset.HasValue) + { + return CreateMediaTypeWithEncoding(mediaType, encoding); + } + + var charsetOffset = charset.Offset - mediaType.Offset; + var restOffset = charsetOffset + charset.Length; + var restLength = mediaType.Length - restOffset; + var finalLength = charsetOffset + encoding.WebName.Length + restLength; + + var builder = new StringBuilder(mediaType.Buffer, mediaType.Offset, charsetOffset, finalLength); + builder.Append(encoding.WebName); + builder.Append(mediaType.Buffer, restOffset, restLength); + + return builder.ToString(); + } + + public static Encoding GetEncoding(string mediaType) + { + return GetEncoding(new StringSegment(mediaType)); + } + + public static Encoding GetEncoding(StringSegment mediaType) + { + var parsedMediaType = new MediaType(mediaType); + return parsedMediaType.Encoding; + } + + /// + /// Creates an containing the media type in + /// and its associated quality. + /// + /// The media type to parse. + /// The position at which the parsing starts. + /// The parsed media type with its associated quality. + public static MediaTypeSegmentWithQuality CreateMediaTypeSegmentWithQuality(string mediaType, int start) + { + var parsedMediaType = new MediaType(mediaType, start, length: null); + var parser = parsedMediaType._parameterParser; + + double quality = 1.0d; + MediaTypeParameter parameter; + while (parser.ParseNextParameter(out parameter)) + { + if (parameter.HasName(QualityParameter)) + { + quality = double.Parse( + parameter.Value.Value, NumberStyles.AllowDecimalPoint, + NumberFormatInfo.InvariantInfo); + } + } + + // We check if the parsed media type has value at this stage when we have iterated + // over all the parameters and we know if the parsing was sucessful. + if (!parser.ParsingFailed) + { + return new MediaTypeSegmentWithQuality( + new StringSegment(mediaType, start, parser.CurrentOffset - start), + quality); + } + else + { + return default(MediaTypeSegmentWithQuality); + } + } + + private static Encoding GetEncodingFromCharset(StringSegment charset) + { + if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) + { + // This is an optimization for utf-8 that prevents the Substring caused by + // charset.Value + return Encoding.UTF8; + } + + try + { + // charset.Value might be an invalid encoding name as in charset=invalid. + // For that reason, we catch the exception thrown by Encoding.GetEncoding + // and return null instead. + return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null; + } + catch (Exception) + { + return null; + } + } + + private static string CreateMediaTypeWithEncoding(StringSegment mediaType, Encoding encoding) + { + return $"{mediaType.Value}; charset={encoding.WebName}"; + } + + private bool ContainsAllParameters(MediaTypeParameterParser setParameters) + { + var parameterFound = true; + MediaTypeParameter setParameter; + while (setParameters.ParseNextParameter(out setParameter) && parameterFound) + { + if (setParameter.HasName("q")) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + // Copy the parser as we need to iterate multiple times over it. + // We can do this because it's a struct + var subSetParameters = _parameterParser; + parameterFound = false; + MediaTypeParameter subSetParameter; + while (subSetParameters.ParseNextParameter(out subSetParameter) && !parameterFound) + { + parameterFound = subSetParameter.Equals(setParameter); + } + } + + return parameterFound; + } + + private struct MediaTypeParameterParser + { + private string _mediaTypeBuffer; + private int? _length; + + public MediaTypeParameterParser(string mediaTypeBuffer, int offset, int? length) + { + _mediaTypeBuffer = mediaTypeBuffer; + _length = length; + CurrentOffset = offset; + ParsingFailed = false; + } + + public int CurrentOffset { get; private set; } + + public bool ParsingFailed { get; private set; } + + public bool ParseNextParameter(out MediaTypeParameter result) + { + if (_mediaTypeBuffer == null) + { + result = default(MediaTypeParameter); + return false; + } + + var parameterLength = GetParameterLength(_mediaTypeBuffer, CurrentOffset, out result); + CurrentOffset = CurrentOffset + parameterLength; + + if (parameterLength == 0) + { + ParsingFailed = _length != null && CurrentOffset < _length; + return false; + } + + return true; + } + + private static int GetParameterLength(string input, int startIndex, out MediaTypeParameter parsedValue) + { + if (OffsetIsOutOfRange(startIndex, input.Length) || + input[startIndex] != ';') + { + parsedValue = default(MediaTypeParameter); + return 0; + } + + StringSegment name; + var nameLength = GetNameLength(input, startIndex, out name); + + var current = startIndex + nameLength; + + if (nameLength == 0 || OffsetIsOutOfRange(current, input.Length) || input[current] != '=') + { + parsedValue = default(MediaTypeParameter); + return 0; + } + + StringSegment value; + var valueLength = GetValueLength(input, current, out value); + + parsedValue = new MediaTypeParameter(name, value); + + current = current + valueLength; + return current - startIndex; + } + + private static int GetNameLength(string input, int startIndex, out StringSegment name) + { + var current = startIndex; + + current++; // skip ';' + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var nameLength = HttpTokenParsingRules.GetTokenLength(input, current); + + if (nameLength == 0) + { + name = default(StringSegment); + return 0; + } + + name = new StringSegment(input, current, nameLength); + + current = current + nameLength; + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + return current - startIndex; + } + + private static int GetValueLength(string input, int startIndex, out StringSegment value) + { + var current = startIndex; + + current++; // skip '='. + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + var valueLength = HttpTokenParsingRules.GetTokenLength(input, current); + + if (valueLength == 0) + { + // A value can either be a token or a quoted string. Check if it is a quoted string. + if (HttpTokenParsingRules.GetQuotedStringLength(input, current, out valueLength) != HttpParseResult.Parsed) + { + // We have an invalid value. Reset the name and return. + value = default(StringSegment); + return 0; + } + } + + value = new StringSegment(input, current, valueLength); + + current = current + valueLength; + current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current); + + return current - startIndex; + } + + private static bool OffsetIsOutOfRange(int offset, int length) + { + return offset < 0 || offset >= length; + } + } + + private struct MediaTypeParameter : IEquatable + { + public static readonly StringSegment Type = new StringSegment("type"); + public static readonly StringSegment Subtype = new StringSegment("subtype"); + + public MediaTypeParameter(StringSegment name, StringSegment value) + { + Name = name; + Value = value; + } + + public StringSegment Name { get; } + + public StringSegment Value { get; } + + public bool HasName(string name) + { + return HasName(new StringSegment(name)); + } + + public bool HasName(StringSegment name) + { + return Name.Equals(name, StringComparison.OrdinalIgnoreCase); + } + + public bool Equals(MediaTypeParameter other) + { + return HasName(other.Name) && + Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is MediaTypeParameter && Equals((MediaTypeParameter)obj); + } + + public override int GetHashCode() + { + HashCodeCombiner hashCode = HashCodeCombiner.Start(); + hashCode.Add(Name.Value); + hashCode.Add(Value.Value); + return hashCode; + } + + public override string ToString() => $"{Name}={Value}"; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeComparisons.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeComparisons.cs deleted file mode 100644 index 33775d5ff4..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeComparisons.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNet.Mvc.Formatters -{ - /// - /// Different types of tests against media type values. - /// - public static class MediaTypeComparisons - { - /// - /// Determines if the media type is a subset of the media type - /// without taking into account the quality parameter. - /// - /// The more general media type. - /// The more specific media type. - /// true if is a more general media type than ; - /// otherwise false. - public static bool IsSubsetOf(StringSegment set, string subset) - { - return IsSubsetOf(set, new StringSegment(subset)); - } - - /// - /// Determines if the media type is a subset of the media type - /// without taking into account the quality parameter. - /// - /// The more general media type. - /// The more specific media type. - /// true if is a more general media type than ; - /// otherwise false. - public static bool IsSubsetOf(string set, string subset) - { - return IsSubsetOf(new StringSegment(set), new StringSegment(subset)); - } - - /// - /// Determines if the media type is a subset of the media type. - /// Two media types are compatible if one is a subset of the other ignoring any charset - /// parameter. - /// - /// The more general media type. - /// The more specific media type. - /// Whether or not we should skip checking the quality parameter. - /// true if is a more general media type than ; - /// otherwise false. - public static bool IsSubsetOf(StringSegment set, StringSegment subset) - { - if (!set.HasValue || !subset.HasValue) - { - return false; - } - - MediaTypeHeaderValue setMediaType; - MediaTypeHeaderValue subSetMediaType; - - return MediaTypeHeaderValue.TryParse(set.Value, out setMediaType) && - MediaTypeHeaderValue.TryParse(subset.Value, out subSetMediaType) && - subSetMediaType.IsSubsetOf(setMediaType); - } - - /// - /// Determines if the type of a given matches all types, E.g, */*. - /// - /// The media type to check - /// true if the matches all subtypes; otherwise false. - public static bool MatchesAllTypes(string mediaType) - { - return MatchesAllTypes(new StringSegment(mediaType)); - } - - /// - /// Determines if the type of a given matches all types, E.g, */*. - /// - /// The media type to check - /// true if the matches all subtypes; otherwise false. - public static bool MatchesAllTypes(StringSegment mediaType) - { - if (!mediaType.HasValue) - { - return false; - } - - MediaTypeHeaderValue parsedMediaType; - return MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType) && - parsedMediaType.MatchesAllTypes; - } - - /// - /// Determines if the given matches all subtypes, E.g, text/*. - /// - /// The media type to check - /// true if the matches all subtypes; otherwise false. - public static bool MatchesAllSubtypes(string mediaType) - { - return MatchesAllSubtypes(new StringSegment(mediaType)); - } - - /// - /// Determines if the given matches all subtypes, E.g, text/*. - /// - /// The media type to check - /// true if the matches all subtypes; otherwise false. - public static bool MatchesAllSubtypes(StringSegment mediaType) - { - if (!mediaType.HasValue) - { - return false; - } - - MediaTypeHeaderValue parsedMediaType; - return MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType) && - parsedMediaType.MatchesAllSubTypes; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs deleted file mode 100644 index c516f92f49..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/MediaTypeEncoding.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Text; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNet.Mvc.Formatters -{ - /// - /// A set of operations to manipulate the encoding of a media type value. - /// - public class MediaTypeEncoding - { - /// - /// Gets the for the given if it exists. - /// - /// The media type from which to get the charset parameter. - /// The of the media type if it exists; otherwise null. - public static Encoding GetEncoding(StringSegment mediaType) - { - var charset = GetCharsetParameter(mediaType); - return GetEncodingFromCharset(charset); - } - - /// - /// Gets the for the given if it exists. - /// - /// The media type from which to get the charset parameter. - /// The of the media type if it exists or a without value if not. - public static Encoding GetEncoding(string mediaType) - { - var charset = GetCharsetParameter(new StringSegment(mediaType)); - return GetEncodingFromCharset(charset); - } - - /// - /// Gets the charset parameter of the given if it exists. - /// - /// The media type from which to get the charset parameter. - /// The charset of the media type if it exists or a without value if not. - public static StringSegment GetCharsetParameter(StringSegment mediaType) - { - MediaTypeHeaderValue parsedMediaType; - if (MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType)) - { - return new StringSegment(parsedMediaType.Charset); - } - return new StringSegment(); - } - - /// - /// Replaces the encoding of the given with the provided - /// . - /// - /// The media type whose encoding will be replaced. - /// The encoding that will replace the encoding in the - /// A media type with the replaced encoding. - public static string ReplaceEncoding(string mediaType, Encoding encoding) - { - return ReplaceEncoding(new StringSegment(mediaType), encoding); - } - - /// - /// Replaces the encoding of the given with the provided - /// . - /// - /// The media type whose encoding will be replaced. - /// The encoding that will replace the encoding in the - /// A media type with the replaced encoding. - public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding) - { - var parsedMediaType = MediaTypeHeaderValue.Parse(mediaType.Value); - - if (string.Equals(parsedMediaType.Encoding?.WebName, encoding?.WebName, StringComparison.OrdinalIgnoreCase)) - { - return mediaType.Value; - } - - parsedMediaType.Encoding = encoding; - return parsedMediaType.ToString(); - } - - private static Encoding GetEncodingFromCharset(StringSegment charset) - { - if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) - { - // This is an optimization for utf-8 that prevents the Substring caused by - // charset.Value - return Encoding.UTF8; - } - - try - { - // charset.Value might be an invalid encoding name as in charset=invalid. - // For that reason, we catch the exception thrown by Encoding.GetEncoding - // and return null instead. - return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null; - } - catch (Exception) - { - return null; - } - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs index 9d538122ef..35bfd1e16a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs @@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Mvc.Formatters var cache = new Dictionary(); foreach (var mediaType in SupportedMediaTypes) { - cache.Add(mediaType, MediaTypeEncoding.ReplaceEncoding(mediaType, Encoding.UTF8)); + cache.Add(mediaType, MediaType.ReplaceEncoding(mediaType, Encoding.UTF8)); } // Safe race condition, worst case scenario we initialize the field multiple times with dictionaries containing @@ -93,11 +93,14 @@ namespace Microsoft.AspNet.Mvc.Formatters { List mediaTypes = null; + var parsedContentType = new MediaType(contentType); + // Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*" // requested and formatter supports "text/plain". Treat contentType like it came from an Accept header. foreach (var mediaType in SupportedMediaTypes) { - if (MediaTypeComparisons.IsSubsetOf(new StringSegment(contentType), mediaType)) + var parsedMediaType = new MediaType(mediaType); + if (parsedMediaType.IsSubsetOf(parsedContentType)) { if (mediaTypes == null) { @@ -135,13 +138,14 @@ namespace Microsoft.AspNet.Mvc.Formatters if (context.ContentType.HasValue) { - var contentTypeEncoding = MediaTypeEncoding.GetCharsetParameter(context.ContentType); - if (contentTypeEncoding.HasValue) + var parsedContentType = new MediaType(context.ContentType); + var contentTypeCharset = parsedContentType.Charset; + if (contentTypeCharset.HasValue) { for (var i = 0; i < SupportedEncodings.Count; i++) { var supportedEncoding = SupportedEncodings[i]; - if (contentTypeEncoding.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) + if (contentTypeCharset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) { // This is supported. return SupportedEncodings[i]; @@ -186,11 +190,11 @@ namespace Microsoft.AspNet.Mvc.Formatters // Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*" // requested and formatter supports "text/plain". contentType is typically what we got in an Accept // header. - var contentType = context.ContentType; + var parsedContentType = new MediaType(context.ContentType); for (var i = 0; i < SupportedMediaTypes.Count; i++) { - var supportedMediaType = SupportedMediaTypes[i]; - if (MediaTypeComparisons.IsSubsetOf(contentType, supportedMediaType)) + var supportedMediaType = new MediaType(SupportedMediaTypes[i]); + if (supportedMediaType.IsSubsetOf(parsedContentType)) { context.ContentType = new StringSegment(SupportedMediaTypes[i]); return true; @@ -272,11 +276,11 @@ namespace Microsoft.AspNet.Mvc.Formatters { if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) && OutputMediaTypeCache.ContainsKey(mediaType)) - { - return OutputMediaTypeCache[mediaType]; - } + { + return OutputMediaTypeCache[mediaType]; + } - return MediaTypeEncoding.ReplaceEncoding(mediaType, encoding); + return MediaType.ReplaceEncoding(mediaType, encoding); } private Encoding MatchAcceptCharacterEncoding(IList acceptCharsetHeaders) diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/StringOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/StringOutputFormatter.cs index a8d0265096..6a8bb81bee 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/StringOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/StringOutputFormatter.cs @@ -55,10 +55,7 @@ namespace Microsoft.AspNet.Mvc.Formatters } var response = context.HttpContext.Response; - - return response.WriteAsync( - valueAsString, - MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8); + return response.WriteAsync(valueAsString, MediaType.GetEncoding(response.ContentType) ?? Encoding.UTF8); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs index 2f6cee1935..c07a263ccb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/ObjectResultExecutor.cs @@ -256,19 +256,11 @@ namespace Microsoft.AspNet.Mvc.Infrastructure HttpRequest request) { var result = new List(); - var parsedHeaders = request.GetTypedHeaders().Accept; - for (var i = 0; i < parsedHeaders?.Count; i++) + AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result); + for (int i = 0; i < result.Count; i++) { - result.Add(new MediaTypeSegmentWithQuality( - new StringSegment(parsedHeaders[i].ToString()), - parsedHeaders[i].Quality ?? 1.0)); - } - - for (var i = 0; i < result.Count; i++) - { - if (!RespectBrowserAcceptHeader && - MediaTypeComparisons.MatchesAllTypes(result[i].MediaType) && - MediaTypeComparisons.MatchesAllSubtypes(result[i].MediaType)) + var mediaType = new MediaType(result[i].MediaType); + if (!RespectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes) { result.Clear(); return result; @@ -292,9 +284,11 @@ namespace Microsoft.AspNet.Mvc.Infrastructure return true; } + var parsedMediaType = new MediaType(mediaType); for (int i = 0; i < acceptableMediaTypes.Count; i++) { - if (MediaTypeComparisons.IsSubsetOf(mediaType, acceptableMediaTypes[i])) + var acceptableMediaType = new MediaType(acceptableMediaTypes[i]); + if (acceptableMediaType.IsSubsetOf(parsedMediaType)) { return true; } @@ -378,7 +372,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure { var mediaType = sortedAcceptHeaders[i]; formatterContext.ContentType = mediaType.MediaType; - for(var j = 0;j < formatters.Count;j++) + for (var j = 0; j < formatters.Count; j++) { var formatter = formatters[j]; if (formatter.CanWriteResult(formatterContext)) @@ -450,8 +444,9 @@ namespace Microsoft.AspNet.Mvc.Infrastructure for (var i = 0; i < contentTypes.Count; i++) { var contentType = contentTypes[i]; - if (MediaTypeComparisons.MatchesAllTypes(contentType) || - MediaTypeComparisons.MatchesAllSubtypes(contentType)) + var parsedContentType = new MediaType(contentType); + if (parsedContentType.MatchesAllTypes || + parsedContentType.MatchesAllSubTypes) { var message = Resources.FormatObjectResult_MatchAllContentType( contentType, diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/ResponseContentTypeHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/ResponseContentTypeHelper.cs index ab7b078d2f..54f3520af1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/ResponseContentTypeHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/ResponseContentTypeHelper.cs @@ -37,14 +37,14 @@ namespace Microsoft.AspNet.Mvc.Internal { Debug.Assert(defaultContentType != null); - var defaultContentTypeEncoding = MediaTypeEncoding.GetEncoding(defaultContentType); + var defaultContentTypeEncoding = MediaType.GetEncoding(defaultContentType); Debug.Assert(defaultContentTypeEncoding != null); // 1. User sets the ContentType property on the action result if (actionResultContentType != null) { resolvedContentType = actionResultContentType; - var actionResultEncoding = MediaTypeEncoding.GetEncoding(actionResultContentType); + var actionResultEncoding = MediaType.GetEncoding(actionResultContentType); resolvedContentTypeEncoding = actionResultEncoding ?? defaultContentTypeEncoding; return; } @@ -52,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.Internal // 2. User sets the ContentType property on the http response directly if (!string.IsNullOrEmpty(httpResponseContentType)) { - var mediaTypeEncoding = MediaTypeEncoding.GetEncoding(httpResponseContentType); + var mediaTypeEncoding = MediaType.GetEncoding(httpResponseContentType); if (mediaTypeEncoding != null) { resolvedContentType = httpResponseContentType; diff --git a/src/Microsoft.AspNet.Mvc.Core/ProducesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ProducesAttribute.cs index 82fdb42719..6210b5b247 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ProducesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ProducesAttribute.cs @@ -92,15 +92,15 @@ namespace Microsoft.AspNet.Mvc var contentTypes = new MediaTypeCollection(); foreach (var arg in completeArgs) { - var contentType = arg; - if (MediaTypeComparisons.MatchesAllSubtypes(contentType)|| - MediaTypeComparisons.MatchesAllTypes(contentType)) + var contentType = new MediaType(arg); + if (contentType.MatchesAllTypes || + contentType.MatchesAllSubTypes) { throw new InvalidOperationException( Resources.FormatMatchAllContentTypeIsNotAllowed(arg)); } - contentTypes.Add(contentType); + contentTypes.Add(arg); } return contentTypes; diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 9307dd86d2..671585b669 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1050,6 +1050,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("FormatFormatterMappings_GetMediaTypeMappingForFormat_InvalidFormat"), p0); } + /// + /// "Invalid values '{0}'." + /// + internal static string AcceptHeaderParser_ParseAcceptHeader_InvalidValues + { + get { return GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"); } + } + + /// + /// "Invalid values '{0}'." + /// + internal static string FormatAcceptHeaderParser_ParseAcceptHeader_InvalidValues(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 946db5d5f7..9f56f1705f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -322,4 +322,7 @@ The argument '{0}' is invalid. Empty or null formats are not supported. + + "Invalid values '{0}'." + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index e16c385e12..6e9f7cf26e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -26,6 +26,10 @@ "type": "build" }, "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", + "Microsoft.Extensions.HashCodeCombiner.Sources": { + "type": "build", + "version": "1.0.0-*" + }, "Microsoft.Extensions.PropertyActivator.Sources": { "version": "1.0.0-*", "type": "build" diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonOutputFormatter.cs index 6a927c3de7..1bafa87b6a 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonOutputFormatter.cs @@ -140,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.Formatters } var response = context.HttpContext.Response; - var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8; + var selectedEncoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8; using (var writer = context.WriterFactory(response.Body, selectedEncoding)) { diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs index e17638161d..f58f5a2c60 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs @@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Mvc.Formatters } var writerSettings = WriterSettings.Clone(); - writerSettings.Encoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8; + writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8; // Wrap the object only if there is a wrapping type. var value = context.Object; diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs index 414de4d775..7ead7c4c1f 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs @@ -165,7 +165,7 @@ namespace Microsoft.AspNet.Mvc.Formatters var response = context.HttpContext.Response; var writerSettings = WriterSettings.Clone(); - writerSettings.Encoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8; + writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8; // Wrap the object only if there is a wrapping type. var value = context.Object; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerBaseTest.cs index c00f95e894..eadfc83df8 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerBaseTest.cs @@ -977,7 +977,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test // Assert Assert.IsType(actualContentResult); Assert.Equal("TestContent", actualContentResult.Content); - Assert.Null(MediaTypeEncoding.GetEncoding(actualContentResult.ContentType)); + Assert.Null(MediaType.GetEncoding(actualContentResult.ContentType)); Assert.Equal("text/plain", actualContentResult.ContentType.ToString()); } @@ -993,7 +993,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test // Assert Assert.IsType(actualContentResult); Assert.Equal("TestContent", actualContentResult.Content); - Assert.Same(Encoding.UTF8, MediaTypeEncoding.GetEncoding(actualContentResult.ContentType)); + Assert.Same(Encoding.UTF8, MediaType.GetEncoding(actualContentResult.ContentType)); Assert.Equal("text/plain; charset=utf-8", actualContentResult.ContentType.ToString()); } @@ -1026,7 +1026,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test Assert.NotNull(contentResult.ContentType); Assert.Equal(contentType, contentResult.ContentType.ToString()); // The default encoding of ContentResult is used when this result is executed. - Assert.Null(MediaTypeEncoding.GetEncoding(contentResult.ContentType)); + Assert.Null(MediaType.GetEncoding(contentResult.ContentType)); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/AcceptHeaderParserTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/AcceptHeaderParserTest.cs new file mode 100644 index 0000000000..4ef9ec7854 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/AcceptHeaderParserTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Formatters +{ + public class AcceptHeaderParserTest + { + [Fact] + public void ParseAcceptHeader_ParsesSimpleHeader() + { + // Arrange + var header = "application/json"; + var expected = new List + { + new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0) + }; + + // Act + var parsed = AcceptHeaderParser.ParseAcceptHeader(new List { header }); + + // Assert + Assert.Equal(expected, parsed); + } + + [Fact] + public void ParseAcceptHeader_ParsesSimpleHeaderWithMultipleValues() + { + // Arrange + var header = "application/json, application/xml;q=0.8"; + var expected = new List + { + new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0), + new MediaTypeSegmentWithQuality(new StringSegment("application/xml;q=0.8"),0.8) + }; + + // Act + var parsed = AcceptHeaderParser.ParseAcceptHeader(new List { header }); + + // Assert + Assert.Equal(expected, parsed); + foreach (var mediaType in parsed) + { + Assert.Same(header, mediaType.MediaType.Buffer); + } + } + + [Fact] + public void ParseAcceptHeader_ParsesMultipleHeaderValues() + { + // Arrange + var expected = new List + { + new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0), + new MediaTypeSegmentWithQuality(new StringSegment("application/xml;q=0.8"),0.8) + }; + + // Act + var parsed = AcceptHeaderParser.ParseAcceptHeader( + new List { "application/json", "", "application/xml;q=0.8" }); + + // Assert + Assert.Equal(expected, parsed); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeComparisonsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeComparisonsTest.cs index 0fa93d3a2a..d16c4a4704 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeComparisonsTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeComparisonsTest.cs @@ -9,57 +9,6 @@ namespace Microsoft.AspNet.Mvc.Formatters { public class MediaTypeComparisonsTest { - [Theory] - [InlineData("application/json", "application/json", true)] - [InlineData("application/json", "application/json;charset=utf-8", true)] - [InlineData("application/json;charset=utf-8", "application/json", false)] - [InlineData("application/json;q=0.8", "application/json;q=0.9", true)] - [InlineData("application/json;q=0.8;charset=utf-7", "application/json;charset=utf-8;q=0.9", true)] - [InlineData("application/json;format=indent;charset=utf-8", "application/json", false)] - [InlineData("application/json", "application/json;format=indent;charset=utf-8", true)] - [InlineData("application/json;format=indent;charset=utf-8", "application/json;format=indent;charset=utf-8", true)] - [InlineData("application/json;charset=utf-8;format=indent", "application/json;format=indent;charset=utf-8", true)] - public void IsSubsetOf(string set, string subset, bool expectedResult) - { - // Arrange & Act - var result = MediaTypeComparisons.IsSubsetOf( - new StringSegment(set), - new StringSegment(subset)); - // Assert - Assert.Equal(expectedResult, result); - } - - [Theory] - [InlineData("*/*", true)] - [InlineData("text/*", false)] - [InlineData("text/plain", false)] - public void MatchesAllTypes(string value, bool expectedResult) - { - // Arrange - var mediaType = new StringSegment(value); - - // Act - var result = MediaTypeComparisons.MatchesAllTypes(mediaType); - - // Assert - Assert.Equal(expectedResult, result); - } - - [Theory] - [InlineData("*/*", true)] - [InlineData("text/*", true)] - [InlineData("text/plain", false)] - public void MatchesAllSubtypes(string value, bool expectedResult) - { - // Arrange - var mediaType = new StringSegment(value); - - // Act - var result = MediaTypeComparisons.MatchesAllSubtypes(mediaType); - - // Assert - Assert.Equal(expectedResult, result); - } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeTest.cs new file mode 100644 index 0000000000..618a431bdd --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/MediaTypeTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Formatters +{ + public class MediaTypeTest + { + [Theory] + [InlineData("application/json")] + [InlineData("application /json")] + public void CanParse_ParameterlessMediaTypes(string mediaType) + { + // Arrange & Act + var result = new MediaType(mediaType, 0, mediaType.Length); + + // Assert + Assert.Equal(new StringSegment("application"), result.Type); + Assert.Equal(new StringSegment("json"), result.SubType); + } + + [Theory] + [InlineData("application/json;format=pretty;charset=utf-8;q=0.8")] + [InlineData("application/json;format=pretty;charset=utf-8; q=0.8 ")] + [InlineData("application/json;format=pretty;charset=utf-8 ; q=0.8 ")] + [InlineData("application/json;format=pretty; charset=utf-8 ; q=0.8 ")] + [InlineData("application/json;format=pretty ; charset=utf-8 ; q=0.8 ")] + [InlineData("application/json; format=pretty ; charset=utf-8 ; q=0.8 ")] + [InlineData("application/json; format=pretty ; charset=utf-8 ; q= 0.8 ")] + [InlineData("application/json; format=pretty ; charset=utf-8 ; q = 0.8 ")] + public void CanParse_MediaTypesWithParameters(string mediaType) + { + // Arrange & Act + var result = new MediaType(mediaType, 0, mediaType.Length); + + // Assert + Assert.Equal(new StringSegment("application"), result.Type); + Assert.Equal(new StringSegment("json"), result.SubType); + Assert.Equal(new StringSegment("pretty"), result.GetParameter("format")); + Assert.Equal(new StringSegment("0.8"), result.GetParameter("q")); + Assert.Equal(new StringSegment("utf-8"), result.GetParameter("charset")); + } + + [Theory] + [InlineData("application/json;charset=utf-8")] + [InlineData("application/json;format=indent;q=0.8;charset=utf-8")] + [InlineData("application/json;format=indent;charset=utf-8;q=0.8")] + [InlineData("application/json;charset=utf-8;format=indent;q=0.8")] + public void GetParameter_ReturnsParameter_IfParameterIsInMediaType(string mediaType) + { + // Arrange + var expectedParameter = new StringSegment("utf-8"); + + var parsedMediaType = new MediaType(mediaType, 0, mediaType.Length); + + // Act + var result = parsedMediaType.GetParameter("charset"); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedParameter, result); + } + + [Fact] + public void GetParameter_IsCaseInsensitive() + { + // Arrange + var mediaType = "application/json;charset=utf-8"; + var expectedParameter = new StringSegment("utf-8"); + + var parsedMediaType = new MediaType(mediaType); + + // Act + var result = parsedMediaType.GetParameter("CHARSET"); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedParameter, result); + } + + [Theory] + [InlineData("application/json", "application/json", true)] + [InlineData("application/json", "application/json;charset=utf-8", true)] + [InlineData("application/json;charset=utf-8", "application/json", false)] + [InlineData("application/json;q=0.8", "application/json;q=0.9", true)] + [InlineData("application/json;q=0.8;charset=utf-7", "application/json;charset=utf-8;q=0.9", true)] + [InlineData("application/json;format=indent;charset=utf-8", "application/json", false)] + [InlineData("application/json", "application/json;format=indent;charset=utf-8", true)] + [InlineData("application/json;format=indent;charset=utf-8", "application/json;format=indent;charset=utf-8", true)] + [InlineData("application/json;charset=utf-8;format=indent", "application/json;format=indent;charset=utf-8", true)] + public void IsSubsetOf(string set, string subset, bool expectedResult) + { + // Arrange + var setMediaType = new MediaType(set); + var subSetMediaType = new MediaType(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("*/*", true)] + [InlineData("text/*", false)] + [InlineData("text/plain", false)] + public void MatchesAllTypes(string value, bool expectedResult) + { + // Arrange + var mediaType = new MediaType(value); + + // Act + var result = mediaType.MatchesAllTypes; + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("*/*", true)] + [InlineData("text/*", true)] + [InlineData("text/plain", false)] + public void MatchesAllSubtypes(string value, bool expectedResult) + { + // Arrange + var mediaType = new MediaType(value); + + // Act + var result = mediaType.MatchesAllSubTypes; + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void GetParameter_ReturnsNull_IfParameterIsNotInMediaType() + { + var mediaType = "application/json;charset=utf-8;format=indent;q=0.8"; + + var parsedMediaType = new MediaType(mediaType, 0, mediaType.Length); + + // Act + var result = parsedMediaType.GetParameter("other"); + + // Assert + Assert.False(result.HasValue); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/ResponseContentTypeHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/ResponseContentTypeHelperTest.cs index a7c69f3358..c32ffe4f23 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/ResponseContentTypeHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/ResponseContentTypeHelperTest.cs @@ -138,7 +138,6 @@ namespace Microsoft.AspNet.Mvc.Internal out resolvedContentTypeEncoding); // Assert - Assert.Equal(expectedContentType, resolvedContentType); Assert.Equal(Encoding.UTF8, resolvedContentTypeEncoding); } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ContentNegotiationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ContentNegotiationTest.cs index 7e834e97f7..2441ab91ea 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ContentNegotiationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ContentNegotiationTest.cs @@ -404,6 +404,50 @@ END:VCARD Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); } + [Fact] + public async Task InvalidResponseContentType_WithNotMatchingAcceptHeader_Returns406() + { + // Arrange + var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson"; + var request = new HttpRequestMessage(HttpMethod.Get, targetUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/custom1")); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + [Fact] + public async Task InvalidResponseContentType_WithMatchingAcceptHeader_Returns406() + { + // Arrange + var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson"; + var request = new HttpRequestMessage(HttpMethod.Get, targetUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + [Fact] + public async Task InvalidResponseContentType_WithoutAcceptHeader_Returns406() + { + // Arrange + var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson"; + var request = new HttpRequestMessage(HttpMethod.Get, targetUri); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + [Fact] public async Task ProducesAttribute_And_FormatFilterAttribute_Conflicting() { diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs index 977c997a5e..3e87a7b784 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc var contentResult = Assert.IsType(result); Assert.Equal(content, contentResult.Content); Assert.Equal("text/asp; charset=us-ascii", contentResult.ContentType.ToString()); - Assert.Equal(encoding, MediaTypeEncoding.GetEncoding(contentResult.ContentType)); + Assert.Equal(encoding, MediaType.GetEncoding(contentResult.ContentType)); } [Theory] diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs index a07a8cfe7a..bf6e129ef1 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs @@ -282,7 +282,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures // Check if the original instance provided by the user has not changed. // Since we do not have access to the new instance created within the view executor, // check if at least the content is the same. - Assert.Null(MediaTypeEncoding.GetEncoding(contentType)); + Assert.Null(MediaType.GetEncoding(contentType)); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs index f5aa58af41..7e73d99c42 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs @@ -265,7 +265,7 @@ namespace System.Web.Http var jsonResult = Assert.IsType(result); Assert.Same(product, jsonResult.Value); - Assert.Same(Encoding.UTF8, MediaTypeEncoding.GetEncoding(jsonResult.ContentType)); + Assert.Same(Encoding.UTF8, MediaType.GetEncoding(jsonResult.ContentType)); } [Fact] diff --git a/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/InvalidContentTypeController.cs b/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/InvalidContentTypeController.cs new file mode 100644 index 0000000000..36ee0dbc28 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/InvalidContentTypeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite.Controllers.ContentNegotiation +{ + public class InvalidContentTypeController : Controller + { + [HttpGet("InvalidContentType/SetResponseContentTypeJson")] + public IActionResult SetResponseContentTypeJson() + { + HttpContext.Response.ContentType = "json"; + return Ok(0); + } + } +} diff --git a/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V3.cs b/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V3.cs index 4a1225efbe..94bf957ec7 100644 --- a/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V3.cs +++ b/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V3.cs @@ -38,7 +38,7 @@ namespace BasicWebSite.Formatters builder.AppendLine(); builder.AppendLine("END:VCARD"); - var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8; + var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8; await context.HttpContext.Response.WriteAsync( builder.ToString(), diff --git a/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V4.cs b/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V4.cs index 875c695db6..e84a50e499 100644 --- a/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V4.cs +++ b/test/WebSites/BasicWebSite/Formatters/VCardFormatter_V4.cs @@ -41,7 +41,7 @@ namespace BasicWebSite.Formatters builder.AppendLine(); builder.AppendLine("END:VCARD"); - var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8; + var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8; await context.HttpContext.Response.WriteAsync( builder.ToString(),