Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
[Fixes #5212] Added a cookie based ITempDataProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
kichalla committed Sep 7, 2016
1 parent df81f8b commit 8b2bced
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 216 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ internal static void AddViewServices(IServiceCollection services)
//
// View Components
//

// These do caching so they should stay singleton
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public void OnResourceExecuting(ResourceExecutingContext context)
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{
_factory.GetTempData(context.HttpContext).Save();
}

/// <inheritdoc />
Expand All @@ -44,6 +43,8 @@ public void OnResultExecuted(ResultExecutedContext context)
{
_factory.GetTempData(context.HttpContext).Keep();
}

_factory.GetTempData(context.HttpContext).Save();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataProviderStore
{
private readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.None
});

private static readonly MethodInfo _convertArrayMethodInfo = typeof(TempDataProviderStore).GetMethod(
nameof(ConvertArray), BindingFlags.Static | BindingFlags.NonPublic);
private static readonly MethodInfo _convertDictMethodInfo = typeof(TempDataProviderStore).GetMethod(
nameof(ConvertDictionary), BindingFlags.Static | BindingFlags.NonPublic);

private static readonly ConcurrentDictionary<Type, Func<JArray, object>> _arrayConverters =
new ConcurrentDictionary<Type, Func<JArray, object>>();
private static readonly ConcurrentDictionary<Type, Func<JObject, object>> _dictionaryConverters =
new ConcurrentDictionary<Type, Func<JObject, object>>();

private static readonly Dictionary<JTokenType, Type> _tokenTypeLookup = new Dictionary<JTokenType, Type>
{
{ JTokenType.String, typeof(string) },
{ JTokenType.Integer, typeof(int) },
{ JTokenType.Boolean, typeof(bool) },
{ JTokenType.Float, typeof(float) },
{ JTokenType.Guid, typeof(Guid) },
{ JTokenType.Date, typeof(DateTime) },
{ JTokenType.TimeSpan, typeof(TimeSpan) },
{ JTokenType.Uri, typeof(Uri) },
};

public IDictionary<string, object> DeserializeTempData(byte[] value)
{
var tempDataDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
using (var memoryStream = new MemoryStream(value))
using (var writer = new BsonReader(memoryStream))
{
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
}

var convertedDictionary = new Dictionary<string, object>(
tempDataDictionary,
StringComparer.OrdinalIgnoreCase);
foreach (var item in tempDataDictionary)
{
var jArrayValue = item.Value as JArray;
var jObjectValue = item.Value as JObject;
if (jArrayValue != null && jArrayValue.Count > 0)
{
var arrayType = jArrayValue[0].Type;
Type returnType;
if (_tokenTypeLookup.TryGetValue(arrayType, out returnType))
{
var arrayConverter = _arrayConverters.GetOrAdd(returnType, type =>
{
return (Func<JArray, object>)_convertArrayMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JArray, object>));
});
var result = arrayConverter(jArrayValue);

convertedDictionary[item.Key] = result;
}
else
{
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), arrayType);
throw new InvalidOperationException(message);
}
}
else if (jObjectValue != null)
{
if (!jObjectValue.HasValues)
{
convertedDictionary[item.Key] = null;
continue;
}

var jTokenType = jObjectValue.Properties().First().Value.Type;
Type valueType;
if (_tokenTypeLookup.TryGetValue(jTokenType, out valueType))
{
var dictionaryConverter = _dictionaryConverters.GetOrAdd(valueType, type =>
{
return (Func<JObject, object>)_convertDictMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JObject, object>));
});
var result = dictionaryConverter(jObjectValue);

convertedDictionary[item.Key] = result;
}
else
{
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), jTokenType);
throw new InvalidOperationException(message);
}
}
else if (item.Value is long)
{
var longValue = (long)item.Value;
if (longValue >= int.MinValue && longValue <= int.MaxValue)
{
// BsonReader casts all ints to longs. We'll attempt to work around this by force converting
// longs to ints when there's no loss of precision.
convertedDictionary[item.Key] = (int)longValue;
}
}
}

return convertedDictionary;
}

public byte[] SerializeTempData(IDictionary<string, object> values)
{
var hasValues = (values != null && values.Count > 0);
if (hasValues)
{
foreach (var item in values.Values)
{
// We want to allow only simple types to be serialized.
EnsureObjectCanBeSerialized(item);
}

using (var memoryStream = new MemoryStream())
{
using (var writer = new BsonWriter(memoryStream))
{
_jsonSerializer.Serialize(writer, values);
return memoryStream.ToArray();
}
}
}
else
{
return new byte[0];
}
}

internal void EnsureObjectCanBeSerialized(object item)
{
var itemType = item.GetType();
Type actualType = null;

if (itemType.IsArray)
{
itemType = itemType.GetElementType();
}
else if (itemType.GetTypeInfo().IsGenericType)
{
if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IList<>)) != null)
{
var genericTypeArguments = itemType.GenericTypeArguments;
Debug.Assert(genericTypeArguments.Length == 1, "IList<T> has one generic argument");
actualType = genericTypeArguments[0];
}
else if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IDictionary<,>)) != null)
{
var genericTypeArguments = itemType.GenericTypeArguments;
Debug.Assert(
genericTypeArguments.Length == 2,
"IDictionary<TKey, TValue> has two generic arguments");

// Throw if the key type of the dictionary is not string.
if (genericTypeArguments[0] != typeof(string))
{
var message = Resources.FormatTempData_CannotSerializeDictionary(
typeof(TempDataProviderStore).FullName, genericTypeArguments[0]);
throw new InvalidOperationException(message);
}
else
{
actualType = genericTypeArguments[1];
}
}
}

actualType = actualType ?? itemType;
if (!IsSimpleType(actualType))
{
var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType;
var message = Resources.FormatTempData_CannotSerializeToSession(
typeof(TempDataProviderStore).FullName, underlyingType);
throw new InvalidOperationException(message);
}
}

private static IList<TVal> ConvertArray<TVal>(JArray array)
{
return array.Values<TVal>().ToArray();
}

private static IDictionary<string, TVal> ConvertDictionary<TVal>(JObject jObject)
{
var convertedDictionary = new Dictionary<string, TVal>(StringComparer.Ordinal);
foreach (var item in jObject)
{
convertedDictionary.Add(item.Key, jObject.Value<TVal>(item.Key));
}
return convertedDictionary;
}

private static bool IsSimpleType(Type type)
{
var typeInfo = type.GetTypeInfo();

return typeInfo.IsPrimitive ||
typeInfo.IsEnum ||
type.Equals(typeof(decimal)) ||
type.Equals(typeof(string)) ||
type.Equals(typeof(DateTime)) ||
type.Equals(typeof(Guid)) ||
type.Equals(typeof(DateTimeOffset)) ||
type.Equals(typeof(TimeSpan)) ||
type.Equals(typeof(Uri));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class CookieTempDataProvider : ITempDataProvider
{
public static readonly string CookieName = ".AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider";

//TODO: DOES THE LENGTH OF THIS NAME MATTER?
private static readonly string Purpose = "Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProviderToken.v1";
private const byte TokenVersion = 0x01;
private readonly IDataProtector _cryptoSystem;
private readonly IDataProtector _dataProtector;
private TempDataProviderStore _tempDataProviderStore;

public CookieTempDataProvider(IDataProtectionProvider dataProtectionProvider)
{
_cryptoSystem = dataProtectionProvider.CreateProtector(Purpose);
_dataProtector = _cryptoSystem.CreateProtector(Purpose);
_tempDataProviderStore = new TempDataProviderStore();
}

public IDictionary<string, object> LoadTempData(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

string value;
if (context.Request.Cookies.TryGetValue(CookieName, out value))
{
var base64EncodedValue = Convert.FromBase64String(value);
var unprotectedData = _dataProtector.Unprotect(base64EncodedValue);
return _tempDataProviderStore.DeserializeTempData(unprotectedData);
}

return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}

public void SaveTempData(HttpContext context, IDictionary<string, object> values)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var hasValues = (values != null && values.Count > 0);
if (hasValues)
{
var bytes = _tempDataProviderStore.SerializeTempData(values);
bytes = _dataProtector.Protect(bytes);

context.Response.Cookies.Append(
CookieName,
Convert.ToBase64String(bytes),
new CookieOptions()
{
//Expires TODO: THE TIME REQUIRED
Path = context.Request.PathBase,
HttpOnly = true,
Secure = true
});
}
else
{
context.Response.Cookies.Delete(CookieName);
}
}
}
}
Loading

0 comments on commit 8b2bced

Please sign in to comment.