File: TempData\CookieTempDataProvider.cs
Web Access
Project: src\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Buffers.Text;
using System.Collections.ObjectModel;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
internal sealed partial class CookieTempDataProvider : ITempDataProvider
{
    public const string CookieName = ".AspNetCore.Components.TempData";
    private const string Purpose = "Microsoft.AspNetCore.Components.CookieTempDataProviderToken";
    private readonly IDataProtector _dataProtector;
    private readonly ISpanDataProtector? _spanDataProtector;
    private readonly ITempDataSerializer _tempDataSerializer;
    private readonly RazorComponentsServiceOptions _options;
    private readonly ChunkingCookieManager _chunkingCookieManager;
    private readonly ILogger<CookieTempDataProvider> _logger;
 
    public CookieTempDataProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<RazorComponentsServiceOptions> options,
        ITempDataSerializer tempDataSerializer,
        ILogger<CookieTempDataProvider> logger)
    {
        _dataProtector = dataProtectionProvider.CreateProtector(Purpose);
        _spanDataProtector = _dataProtector as ISpanDataProtector;
        _tempDataSerializer = tempDataSerializer;
        _options = options.Value;
        _chunkingCookieManager = new ChunkingCookieManager();
        _logger = logger;
    }
 
    public IDictionary<string, object?> LoadTempData(HttpContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
        var cookieName = _options.TempDataCookie.Name ?? CookieName;
 
        try
        {
            if (!context.Request.Cookies.ContainsKey(cookieName))
            {
                Log.TempDataCookieNotFound(_logger, cookieName);
                return ReadOnlyDictionary<string, object?>.Empty;
            }
            var serializedDataFromCookie = _chunkingCookieManager.GetRequestCookie(context, cookieName);
            if (serializedDataFromCookie is null)
            {
                return ReadOnlyDictionary<string, object?>.Empty;
            }
 
            byte[]? rentedDecodeBuffer = null;
            var maxDecodedSize = Base64Url.GetMaxDecodedLength(serializedDataFromCookie.Length);
            var decodeBuffer = maxDecodedSize <= 256
                ? stackalloc byte[256]
                : (rentedDecodeBuffer = ArrayPool<byte>.Shared.Rent(maxDecodedSize));
 
            try
            {
                var decodeStatus = Base64Url.DecodeFromChars(serializedDataFromCookie, decodeBuffer, out _, out var bytesWritten);
                var protectedBytes = decodeBuffer[..bytesWritten];
                Dictionary<string, JsonElement>? dataFromCookie;
 
                if (_spanDataProtector is not null)
                {
                    var unprotectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]);
                    _spanDataProtector.Unprotect(protectedBytes, ref unprotectBuffer);
                    dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(unprotectBuffer.WrittenSpan);
                }
                else
                {
                    var unprotectedBytes = _dataProtector.Unprotect(protectedBytes.ToArray());
                    dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(unprotectedBytes);
                }
 
                if (dataFromCookie is null)
                {
                    return ReadOnlyDictionary<string, object?>.Empty;
                }
                var convertedData = _tempDataSerializer.DeserializeData(dataFromCookie);
                Log.TempDataCookieLoadSuccess(_logger, cookieName);
                return convertedData;
            }
            finally
            {
                if (rentedDecodeBuffer is not null)
                {
                    ArrayPool<byte>.Shared.Return(rentedDecodeBuffer);
                }
            }
        }
        catch (Exception ex)
        {
            Log.TempDataCookieLoadFailure(_logger, cookieName, ex);
 
            var cookieOptions = _options.TempDataCookie.Build(context);
            SetCookiePath(context, cookieOptions);
            context.Response.Cookies.Delete(cookieName, cookieOptions);
            return ReadOnlyDictionary<string, object?>.Empty;
        }
    }
 
    public void SaveTempData(HttpContext context, IDictionary<string, object?> values)
    {
        ArgumentNullException.ThrowIfNull(context);
 
        foreach (var kvp in values)
        {
            if (kvp.Value is not null && !_tempDataSerializer.CanSerialize(kvp.Value.GetType()))
            {
                throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value.GetType()}'.");
            }
        }
 
        var cookieName = _options.TempDataCookie.Name ?? CookieName;
        var cookieOptions = _options.TempDataCookie.Build(context);
        SetCookiePath(context, cookieOptions);
 
        if (values.Count == 0)
        {
            _chunkingCookieManager.DeleteCookie(context, cookieName, cookieOptions);
            return;
        }
 
        var bytes = _tempDataSerializer.SerializeData(values);
        var protectedBytes = _dataProtector.Protect(bytes);
        var encodedValue = Base64Url.EncodeToString(protectedBytes);
        _chunkingCookieManager.AppendResponseCookie(context, cookieName, encodedValue, cookieOptions);
        Log.TempDataCookieSaveSuccess(_logger, cookieName);
    }
 
    private void SetCookiePath(HttpContext httpContext, CookieOptions cookieOptions)
    {
        if (!string.IsNullOrEmpty(_options.TempDataCookie.Path))
        {
            cookieOptions.Path = _options.TempDataCookie.Path;
        }
        else
        {
            var pathBase = httpContext.Request.PathBase.ToString();
            if (!string.IsNullOrEmpty(pathBase))
            {
                cookieOptions.Path = pathBase;
            }
        }
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Debug, "The temp data cookie {CookieName} was not found.", EventName = "TempDataCookieNotFound")]
        public static partial void TempDataCookieNotFound(ILogger logger, string cookieName);
 
        [LoggerMessage(2, LogLevel.Warning, "The temp data cookie {CookieName} could not be loaded.", EventName = "TempDataCookieLoadFailure")]
        public static partial void TempDataCookieLoadFailure(ILogger logger, string cookieName, Exception exception);
 
        [LoggerMessage(3, LogLevel.Debug, "The temp data cookie {CookieName} was successfully saved.", EventName = "TempDataCookieSaveSuccess")]
        public static partial void TempDataCookieSaveSuccess(ILogger logger, string cookieName);
 
        [LoggerMessage(4, LogLevel.Debug, "The temp data cookie {CookieName} was successfully loaded.", EventName = "TempDataCookieLoadSuccess")]
        public static partial void TempDataCookieLoadSuccess(ILogger logger, string cookieName);
    }
}