diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/CookieAcceptanceMiddlewareTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/CookieAcceptanceMiddlewareTests.cs new file mode 100644 index 000000000..77f1eacc7 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/CookieAcceptanceMiddlewareTests.cs @@ -0,0 +1,74 @@ +using Moq; +using Microsoft.AspNetCore.Http; + +namespace CO.CDP.OrganisationApp.Tests; + +public class CookieAcceptanceMiddlewareTests +{ + private readonly Mock<ICookiePreferencesService> _cookiePreferencesServiceMock; + private readonly Mock<RequestDelegate> _nextDelegateMock; + private readonly CookieAcceptanceMiddleware _middleware; + private readonly DefaultHttpContext _httpContext; + + public CookieAcceptanceMiddlewareTests() + { + _cookiePreferencesServiceMock = new Mock<ICookiePreferencesService>(); + _nextDelegateMock = new Mock<RequestDelegate>(); + _middleware = new CookieAcceptanceMiddleware(_cookiePreferencesServiceMock.Object); + _httpContext = new DefaultHttpContext(); + } + + [Fact] + public async Task InvokeAsync_ShouldCallAccept_WhenQueryParameterIsTrue() + { + _httpContext.Request.QueryString = new QueryString($"?{CookieSettings.FtsHandoverParameter}=true"); + + await _middleware.InvokeAsync(_httpContext, _nextDelegateMock.Object); + + _cookiePreferencesServiceMock.Verify(s => s.Accept(), Times.Once); + _nextDelegateMock.Verify(n => n(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldCallReject_WhenQueryParameterIsFalse() + { + _httpContext.Request.QueryString = new QueryString($"?{CookieSettings.FtsHandoverParameter}=false"); + + await _middleware.InvokeAsync(_httpContext, _nextDelegateMock.Object); + + _cookiePreferencesServiceMock.Verify(s => s.Reject(), Times.Once); + _nextDelegateMock.Verify(n => n(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldCallReset_WhenQueryParameterIsUnknown() + { + _httpContext.Request.QueryString = new QueryString($"?{CookieSettings.FtsHandoverParameter}=unknown"); + + await _middleware.InvokeAsync(_httpContext, _nextDelegateMock.Object); + + _cookiePreferencesServiceMock.Verify(s => s.Reset(), Times.Once); + _nextDelegateMock.Verify(n => n(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldNotCallAnyMethod_WhenQueryParameterIsMissing() + { + await _middleware.InvokeAsync(_httpContext, _nextDelegateMock.Object); + + _cookiePreferencesServiceMock.VerifyNoOtherCalls(); + _nextDelegateMock.Verify(n => n(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_ShouldNotCallAnyMethod_WhenQueryParameterHasInvalidValue() + { + _httpContext.Request.QueryString = new QueryString($"?{CookieSettings.FtsHandoverParameter}=invalid"); + + await _middleware.InvokeAsync(_httpContext, _nextDelegateMock.Object); + + _cookiePreferencesServiceMock.VerifyNoOtherCalls(); + _nextDelegateMock.Verify(n => n(_httpContext), Times.Once); + } +} + diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/CookiePreferencesServiceTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/CookiePreferencesServiceTests.cs new file mode 100644 index 000000000..76f1d074c --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/CookiePreferencesServiceTests.cs @@ -0,0 +1,103 @@ +using Moq; +using FluentAssertions; +using Microsoft.AspNetCore.Http; + +namespace CO.CDP.OrganisationApp.Tests; + +public class CookiePreferencesServiceTests +{ + private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock; + private readonly Mock<HttpContext> _httpContextMock; + private readonly Mock<HttpRequest> _httpRequestMock; + private readonly Mock<HttpResponse> _httpResponseMock; + private readonly Mock<IResponseCookies> _responseCookiesMock; + private readonly Mock<IRequestCookieCollection> _requestCookiesMock; + private readonly CookiePreferencesService _cookiePreferencesService; + + public CookiePreferencesServiceTests() + { + _httpContextAccessorMock = new Mock<IHttpContextAccessor>(); + _httpContextMock = new Mock<HttpContext>(); + _httpRequestMock = new Mock<HttpRequest>(); + _httpResponseMock = new Mock<HttpResponse>(); + _responseCookiesMock = new Mock<IResponseCookies>(); + _requestCookiesMock = new Mock<IRequestCookieCollection>(); + + _httpContextMock.Setup(c => c.Request).Returns(_httpRequestMock.Object); + _httpContextMock.Setup(c => c.Response).Returns(_httpResponseMock.Object); + _httpRequestMock.Setup(r => r.Cookies).Returns(_requestCookiesMock.Object); + _httpResponseMock.Setup(r => r.Cookies).Returns(_responseCookiesMock.Object); + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(_httpContextMock.Object); + + _cookiePreferencesService = new CookiePreferencesService(_httpContextAccessorMock.Object); + } + + [Fact] + public void Constructor_ShouldThrowException_WhenHttpContextIsNull() + { + var accessor = new Mock<IHttpContextAccessor>(); + accessor.Setup(a => a.HttpContext).Returns((HttpContext)null!); + + Action act = () => new CookiePreferencesService(accessor.Object); + + act.Should().Throw<InvalidOperationException>() + .WithMessage("No active HTTP context."); + } + + [Fact] + public void Accept_ShouldSetCookieWithAcceptValue() + { + _cookiePreferencesService.Accept(); + + _responseCookiesMock.Verify(c => c.Append(CookieSettings.CookieName, "Accept", It.IsAny<CookieOptions>()), Times.Once); + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Accept); + _cookiePreferencesService.IsAccepted().Should().BeTrue(); + } + + [Fact] + public void Reject_ShouldSetCookieWithRejectValue() + { + _cookiePreferencesService.Reject(); + + _responseCookiesMock.Verify(c => c.Append(CookieSettings.CookieName, "Reject", It.IsAny<CookieOptions>()), Times.Once); + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Reject); + _cookiePreferencesService.IsRejected().Should().BeTrue(); + } + + [Fact] + public void Reset_ShouldDeleteCookie_AndSetPendingValueToUnknown() + { + _cookiePreferencesService.Reset(); + + _responseCookiesMock.Verify(c => c.Delete(CookieSettings.CookieName), Times.Once); + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Unknown); + _cookiePreferencesService.IsUnknown().Should().BeTrue(); + } + + [Fact] + public void GetValue_ShouldReturnPendingValue_WhenSet() + { + _cookiePreferencesService.Accept(); + + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Accept); + } + + [Fact] + public void GetValue_ShouldReturnValueFromRequestCookie_WhenNoPendingValue() + { + _requestCookiesMock.Setup(c => c.ContainsKey(CookieSettings.CookieName)).Returns(true); + _requestCookiesMock.Setup(c => c[CookieSettings.CookieName]).Returns("Reject"); + + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Reject); + } + + [Fact] + public void GetValue_ShouldReturnUnknown_WhenCookieIsNotPresent() + { + _requestCookiesMock.Setup(c => c.ContainsKey(CookieSettings.CookieName)).Returns(false); + + _cookiePreferencesService.GetValue().Should().Be(CookieAcceptanceValues.Unknown); + } +} + diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs index 03991869a..6de546262 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs @@ -4,16 +4,19 @@ using FluentAssertions; namespace CO.CDP.OrganisationApp.Tests; + public class FtsUrlServiceTests { private readonly Mock<IConfiguration> _configurationMock; - private readonly FtsUrlService _service; + private readonly Mock<ICookiePreferencesService> _cookiePreferencesService; + private readonly IFtsUrlService _service; public FtsUrlServiceTests() { _configurationMock = new Mock<IConfiguration>(); + _cookiePreferencesService = new Mock<ICookiePreferencesService>(); _configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/"); - _service = new FtsUrlService(_configurationMock.Object); + _service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); } [Fact] @@ -21,23 +24,23 @@ public void Constructor_ShouldThrowException_WhenFtsServiceIsNotConfigured() { _configurationMock.Setup(c => c["FtsService"]).Returns((string?)null); - Action action = () => new FtsUrlService(_configurationMock.Object); + Action action = () => new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); action.Should().Throw<InvalidOperationException>() - .WithMessage("FtsService is not configured."); + .WithMessage("FtsService is not configured."); } [Fact] public void BuildUrl_ShouldTrimTrailingSlashFromBaseServiceUrl() { _configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/"); - var service = new FtsUrlService(_configurationMock.Object); + var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); var endpoint = "test-endpoint"; CultureInfo.CurrentUICulture = new CultureInfo("en-GB"); var result = service.BuildUrl(endpoint); - result.Should().Be("https://example.com/test-endpoint?language=en_GB"); + result.Should().Be("https://example.com/test-endpoint?language=en_GB&cookies_accepted=unknown"); } [Fact] @@ -48,18 +51,18 @@ public void BuildUrl_ShouldConstructCorrectUrl_WhenOnlyEndpointIsProvided() var result = _service.BuildUrl(endpoint); - result.Should().Be("https://example.com/test-endpoint?language=en_GB"); + result.Should().Be("https://example.com/test-endpoint?language=en_GB&cookies_accepted=unknown"); } [Fact] - public void BuildUrl_ShouldConstructCorrectUrl_WhenLanguageisWelsh() + public void BuildUrl_ShouldConstructCorrectUrl_WhenLanguageIsWelsh() { var endpoint = "test-endpoint"; CultureInfo.CurrentUICulture = new CultureInfo("cy"); var result = _service.BuildUrl(endpoint); - result.Should().Be("https://example.com/test-endpoint?language=cy"); + result.Should().Be("https://example.com/test-endpoint?language=cy&cookies_accepted=unknown"); } [Fact] @@ -71,7 +74,7 @@ public void BuildUrl_ShouldIncludeOrganisationId_WhenProvided() var result = _service.BuildUrl(endpoint, organisationId); - result.Should().Be($"https://example.com/test-endpoint?language=en_GB&organisation_id={organisationId}"); + result.Should().Be($"https://example.com/test-endpoint?language=en_GB&organisation_id={organisationId}&cookies_accepted=unknown"); } [Fact] @@ -83,12 +86,13 @@ public void BuildUrl_ShouldIncludeRedirectUrl_WhenProvided() var result = _service.BuildUrl(endpoint, null, redirectUrl); - result.Should().Be("https://example.com/test-endpoint?language=en_GB&redirect_url=%2Fredirect-path"); + result.Should().Be("https://example.com/test-endpoint?language=en_GB&redirect_url=%2Fredirect-path&cookies_accepted=unknown"); } [Fact] public void BuildUrl_ShouldIncludeAllParameters_WhenAllAreProvided() { + _cookiePreferencesService.Setup(c => c.GetValue()).Returns(CookieAcceptanceValues.Accept); var endpoint = "test-endpoint"; var organisationId = Guid.NewGuid(); var redirectUrl = "/redirect-path"; @@ -96,6 +100,21 @@ public void BuildUrl_ShouldIncludeAllParameters_WhenAllAreProvided() var result = _service.BuildUrl(endpoint, organisationId, redirectUrl); - result.Should().Be($"https://example.com/test-endpoint?language=en_GB&organisation_id={organisationId}&redirect_url=%2Fredirect-path"); + result.Should().Be($"https://example.com/test-endpoint?language=en_GB&organisation_id={organisationId}&redirect_url=%2Fredirect-path&cookies_accepted=true"); + } + + [Theory] + [InlineData(CookieAcceptanceValues.Accept, "true")] + [InlineData(CookieAcceptanceValues.Reject, "false")] + [InlineData(CookieAcceptanceValues.Unknown, "unknown")] + public void BuildUrl_ShouldIncludeCookiesAcceptedParameter_BasedOnCookiePreferences(CookieAcceptanceValues preference, string expectedValue) + { + _cookiePreferencesService.Setup(c => c.GetValue()).Returns(preference); + var endpoint = "test-endpoint"; + CultureInfo.CurrentUICulture = new CultureInfo("en-GB"); + + var result = _service.BuildUrl(endpoint); + + result.Should().Be($"https://example.com/test-endpoint?language=en_GB&cookies_accepted={expectedValue}"); } } diff --git a/Frontend/CO.CDP.OrganisationApp/CookieAcceptanceMiddleware.cs b/Frontend/CO.CDP.OrganisationApp/CookieAcceptanceMiddleware.cs new file mode 100644 index 000000000..7546418e5 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/CookieAcceptanceMiddleware.cs @@ -0,0 +1,27 @@ +namespace CO.CDP.OrganisationApp; + +public class CookieAcceptanceMiddleware(ICookiePreferencesService cookiePreferencesService) : IMiddleware +{ + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (context.Request.Query.TryGetValue(CookieSettings.FtsHandoverParameter, out var cookiesAcceptedValue)) + { + string cookiesAccepted = cookiesAcceptedValue.ToString().ToLower(); + + switch (cookiesAccepted) + { + case "true": + cookiePreferencesService.Accept(); + break; + case "false": + cookiePreferencesService.Reject(); + break; + case "unknown": + cookiePreferencesService.Reset(); + break; + } + } + + await next(context); + } +} diff --git a/Frontend/CO.CDP.OrganisationApp/CookiePreferencesService.cs b/Frontend/CO.CDP.OrganisationApp/CookiePreferencesService.cs new file mode 100644 index 000000000..840a10750 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/CookiePreferencesService.cs @@ -0,0 +1,91 @@ +namespace CO.CDP.OrganisationApp; + +public class CookiePreferencesService : ICookiePreferencesService +{ + private readonly HttpContext _context; + + private CookieAcceptanceValues? pendingAcceptanceValue; + + public CookiePreferencesService(IHttpContextAccessor httpContextAccessor) + { + _context = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("No active HTTP context."); + } + + public void Accept() + { + SetCookie(CookieAcceptanceValues.Accept); + } + + public void Reject() + { + SetCookie(CookieAcceptanceValues.Reject); + } + + public void Reset() + { + _context.Response.Cookies.Delete(CookieSettings.CookieName); + pendingAcceptanceValue = CookieAcceptanceValues.Unknown; + } + + private void SetCookie(CookieAcceptanceValues value) + { + _context.Response.Cookies.Append(CookieSettings.CookieName, value.ToString(), new CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(365), + IsEssential = true, + HttpOnly = true, + Secure = _context.Request.IsHttps + }); + pendingAcceptanceValue = value; + } + + public CookieAcceptanceValues GetValue() + { + if (pendingAcceptanceValue != null) + { + return (CookieAcceptanceValues)pendingAcceptanceValue; + } + + if(_context.Request.Cookies.ContainsKey(CookieSettings.CookieName)) + { + if (Enum.TryParse(typeof(CookieAcceptanceValues), _context.Request.Cookies[CookieSettings.CookieName], true, out var result)) + { + return (CookieAcceptanceValues)result; + } + } + + return CookieAcceptanceValues.Unknown; + + } + + public bool IsAccepted() + { + return GetValue() == CookieAcceptanceValues.Accept; + } + + public bool IsRejected() + { + return GetValue() == CookieAcceptanceValues.Reject; + } + + public bool IsUnknown() + { + return GetValue() == CookieAcceptanceValues.Unknown; + } +} +public enum CookieAcceptanceValues +{ + Unknown, + Accept, + Reject +} + +public static class CookieSettings +{ + public const string CookieAcceptanceFieldName = "CookieAcceptance"; + public const string CookieSettingsPageReturnUrlFieldName = "ReturnUrl"; + public const string CookieBannerInteractionQueryString = "cookieBannerInteraction"; + public const string CookieName = "SIRSI_COOKIES_PREFERENCES_SET"; + public const string FtsHandoverParameter = "cookies_accepted"; +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs b/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs index d6a512ab5..1410bb96c 100644 --- a/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs +++ b/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs @@ -1,17 +1,18 @@ using Microsoft.AspNetCore.Http.Extensions; using System.Globalization; -using System.Web; namespace CO.CDP.OrganisationApp; public class FtsUrlService : IFtsUrlService { private readonly string _ftsService; + private readonly ICookiePreferencesService _cookiePreferencesService; - public FtsUrlService(IConfiguration configuration) + public FtsUrlService(IConfiguration configuration, ICookiePreferencesService cookiePreferencesService) { var ftsService = configuration["FtsService"] ?? throw new InvalidOperationException("FtsService is not configured."); _ftsService = ftsService.TrimEnd('/'); + _cookiePreferencesService = cookiePreferencesService; } public string BuildUrl(string endpoint, Guid? organisationId = null, string? redirectUrl = null) @@ -36,13 +37,18 @@ public string BuildUrl(string endpoint, Guid? organisationId = null, string? red queryBuilder.Add("redirect_url", redirectUrl); } + CookieAcceptanceValues cookiesAccepted = _cookiePreferencesService.GetValue(); + string cookiesAcceptedValue = cookiesAccepted switch + { + CookieAcceptanceValues.Accept => "true", + CookieAcceptanceValues.Reject => "false", + _ => "unknown" + }; + + queryBuilder.Add(CookieSettings.FtsHandoverParameter, cookiesAcceptedValue); + uriBuilder.Query = queryBuilder.ToQueryString().Value; return uriBuilder.Uri.ToString(); } } - -public interface IFtsUrlService -{ - string BuildUrl(string endpoint, Guid? organisationId = null, string? redirectUrl = null); -} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/ICookiePreferencesService.cs b/Frontend/CO.CDP.OrganisationApp/ICookiePreferencesService.cs new file mode 100644 index 000000000..fce705800 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/ICookiePreferencesService.cs @@ -0,0 +1,12 @@ +namespace CO.CDP.OrganisationApp; + +public interface ICookiePreferencesService +{ + bool IsAccepted(); + bool IsRejected(); + void Accept(); + void Reject(); + void Reset(); + bool IsUnknown(); + CookieAcceptanceValues GetValue(); +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/IFtsUrlService.cs b/Frontend/CO.CDP.OrganisationApp/IFtsUrlService.cs new file mode 100644 index 000000000..068501e2a --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/IFtsUrlService.cs @@ -0,0 +1,6 @@ +namespace CO.CDP.OrganisationApp; + +public interface IFtsUrlService +{ + string BuildUrl(string endpoint, Guid? organisationId = null, string? redirectUrl = null); +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml index fd1a24641..f6630a0b2 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml @@ -4,6 +4,8 @@ @using CO.CDP.Localization @using Microsoft.AspNetCore.Mvc.Localization +@inject ICookiePreferencesService cookiePreferencesService; + @{ ViewData["Title"] = StaticTextResource.Supplementary_Cookies_Title; var hasError = ((TagBuilder)Html.ValidationMessageFor(m => m.CookieAcceptance)).HasInnerHtml; @@ -45,13 +47,13 @@ <div class="govuk-radios govuk-!-margin-bottom-8" data-module="govuk-radios"> <div class="govuk-radios__item"> - <input class="govuk-radios__input" id="@nameof(Model.CookieAcceptance)" name="@nameof(Model.CookieAcceptance)" type="radio" value="@CookieAcceptanceValues.Accept" checked="@Model.RadioIsChecked(CookieAcceptanceValues.Accept)"> + <input class="govuk-radios__input" id="@nameof(Model.CookieAcceptance)" name="@nameof(Model.CookieAcceptance)" type="radio" value="@CookieAcceptanceValues.Accept" checked="@cookiePreferencesService.IsAccepted()"> <label class="govuk-label govuk-radios__label" for="@nameof(Model.CookieAcceptance)"> @StaticTextResource.Supplementary_Cookies_UseCookies </label> </div> <div class="govuk-radios__item"> - <input class="govuk-radios__input" id="@nameof(Model.CookieAcceptance)-1" name="@nameof(Model.CookieAcceptance)" type="radio" value="@CookieAcceptanceValues.Reject" checked="@Model.RadioIsChecked(CookieAcceptanceValues.Reject)"> + <input class="govuk-radios__input" id="@nameof(Model.CookieAcceptance)-1" name="@nameof(Model.CookieAcceptance)" type="radio" value="@CookieAcceptanceValues.Reject" checked="@cookiePreferencesService.IsRejected()"> <label class="govuk-label govuk-radios__label" for="@nameof(Model.CookieAcceptance)-1"> @StaticTextResource.Supplementary_Cookies_DoNotUseCookies </label> diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml.cs index 7e49df3bc..5a25ae200 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Cookies.cshtml.cs @@ -7,8 +7,8 @@ namespace CO.CDP.OrganisationApp.Pages; [AuthenticatedSessionNotRequired] public class CookiesModel( - IWebHostEnvironment env, - IFlashMessageService flashMessageService) : PageModel + IFlashMessageService flashMessageService, + ICookiePreferencesService cookiePreferencesService) : PageModel { [BindProperty] [Required(ErrorMessage="Choose whether you accept cookies that measure website use")] @@ -36,43 +36,24 @@ public IActionResult OnPost() return Page(); } - Response.Cookies.Append(CookieSettings.CookieName, ((int)CookieAcceptance).ToString(), new CookieOptions{ - Expires = DateTimeOffset.UtcNow.AddDays(365), - IsEssential = true, - HttpOnly = false, - Secure = !env.IsDevelopment() - }); + switch(CookieAcceptance) + { + case CookieAcceptanceValues.Accept: + cookiePreferencesService.Accept(); + break; + + case CookieAcceptanceValues.Reject: + cookiePreferencesService.Reject(); + break; + } if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl)) { - return LocalRedirect(QueryHelpers.AddQueryString(ReturnUrl, CookieSettings.CookiesAcceptedQueryString, "true")); + return LocalRedirect(QueryHelpers.AddQueryString(ReturnUrl, CookieSettings.CookieBannerInteractionQueryString, "true")); } + flashMessageService.SetFlashMessage(FlashMessageType.Success, "You’ve set your cookie preferences."); return RedirectToPage("/Cookies"); } - - public bool RadioIsChecked(CookieAcceptanceValues value) - { - return Request.Cookies.ContainsKey(CookieSettings.CookieName) && Request.Cookies[CookieSettings.CookieName] == ((int)value).ToString(); - } -} - -public enum CookieAcceptanceValues -{ - Accept=1, - Reject=2 -} - -public static class CookieSettings -{ - public const string CookieAcceptanceFieldName = "CookieAcceptance"; - public const string CookieSettingsPageReturnUrlFieldName = "ReturnUrl"; - public const string CookiesAcceptedQueryString = "cookiesAccepted"; - - // Cookie name in FTS is FT_COOKIES_PREFERENCES_SET - // Cookie values have been configured to match (See the 1 and 2 in the CookieAcceptanceValues enum). - // So if we're sharing a domain in production,we could switch to using the same cookie name and remove the frontend page - // (But keep the post handler for setting it - otherwise our cookie banner would need to post across to their subdomain) - public const string CookieName = "SIRSI_COOKIES_PREFERENCES_SET"; } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_CookieBanner.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_CookieBanner.cshtml index 2ed94f1e9..1f15fb4d5 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_CookieBanner.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_CookieBanner.cshtml @@ -1,8 +1,18 @@ +@using Microsoft.AspNetCore.Http.Extensions; +@using Microsoft.AspNetCore.Http; +@using Microsoft.AspNetCore.WebUtilities; +@inject ICookiePreferencesService cookiePreferencesService; + @{ - string currentUrl = Context.Request.PathBase + Context.Request.Path + Context.Request.QueryString; + var queryDict = QueryHelpers.ParseQuery(Context.Request.QueryString.Value); + queryDict.Remove(CookieSettings.FtsHandoverParameter); + var queryBuilder = new QueryBuilder(queryDict); + QueryString updatedQueryString = queryBuilder.ToQueryString(); + string currentUrl = Context.Request.PathBase + Context.Request.Path + updatedQueryString; } -@if (!Context.Request.Cookies.ContainsKey(CookieSettings.CookieName) && !Context.Request.Path.StartsWithSegments("/cookies")) { +@if (cookiePreferencesService.IsUnknown() && !Context.Request.Query.ContainsKey(CookieSettings.CookieBannerInteractionQueryString) && !Context.Request.Path.StartsWithSegments("/cookies")) +{ <div class="govuk-cookie-banner" data-nosnippet role="region" aria-label="Cookies on Find a Tender"> <div class="govuk-cookie-banner__message govuk-width-container"> <div class="govuk-grid-row"> @@ -32,9 +42,9 @@ </div> } -@if (Context.Request.Cookies.ContainsKey(CookieSettings.CookieName) && Context.Request.Query.ContainsKey(CookieSettings.CookiesAcceptedQueryString)) +@if (!cookiePreferencesService.IsUnknown() && Context.Request.Query.ContainsKey(CookieSettings.CookieBannerInteractionQueryString)) { - var acceptedState = (Context.Request.Cookies.ContainsKey(CookieSettings.CookieName) && Context.Request.Cookies[CookieSettings.CookieName] == ((int)CookieAcceptanceValues.Accept).ToString()) ? "accepted" : "rejected"; + var acceptedState = cookiePreferencesService.IsAccepted() ? "accepted" : "rejected"; <div class="govuk-cookie-banner" data-nosnippet role="region" aria-label="Cookies on Find a Tender"> <div class="govuk-cookie-banner__message govuk-width-container"> <div class="govuk-grid-row"> @@ -47,7 +57,7 @@ <div class="govuk-button-group"> @{ var currentQueryString = Context.Request.QueryString.ToString(); - var cleanedQueryString = currentQueryString.Replace($"{CookieSettings.CookiesAcceptedQueryString}=true", ""); + var cleanedQueryString = currentQueryString.Replace($"{CookieSettings.CookieBannerInteractionQueryString}=true", ""); cleanedQueryString = cleanedQueryString == "?" ? "" : cleanedQueryString; } <a class="govuk-button" role="button" data-module="govuk-button" href="@(Context.Request.PathBase + Context.Request.Path + cleanedQueryString)">Hide cookie message</a> diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_Layout.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_Layout.cshtml index ea3437209..72f1c06d9 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_Layout.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Shared/_Layout.cshtml @@ -5,6 +5,7 @@ @inject Microsoft.Extensions.Configuration.IConfiguration config @inject IUserInfoService userInfoService +@inject ICookiePreferencesService cookiePreferencesService; @inject IHtmlLocalizer<StaticTextResource> Localizer @inject IFtsUrlService FtsUrlService @@ -14,8 +15,7 @@ @{ var googleTagId = config["GoogleTagId"]; if (!string.IsNullOrWhiteSpace(googleTagId) - && Context.Request.Cookies.ContainsKey(CookieSettings.CookieName) - && Context.Request.Cookies[CookieSettings.CookieName] == ((int)CookieAcceptanceValues.Accept).ToString()) + && cookiePreferencesService.IsAccepted()) { <!-- Google tag (gtag.js) --> <script async src="https://www.googletagmanager.com/gtag/js?id=@googleTagId"></script> diff --git a/Frontend/CO.CDP.OrganisationApp/Program.cs b/Frontend/CO.CDP.OrganisationApp/Program.cs index 944b54b21..8a320e952 100644 --- a/Frontend/CO.CDP.OrganisationApp/Program.cs +++ b/Frontend/CO.CDP.OrganisationApp/Program.cs @@ -218,6 +218,8 @@ builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationMiddlewareResultHandler>(); builder.Services.AddAuthorization(); +builder.Services.AddScoped<CookieAcceptanceMiddleware>(); +builder.Services.AddScoped<ICookiePreferencesService, CookiePreferencesService>(); builder.Services.AddScoped<IFlashMessageService, FlashMessageService>(); builder.Services.AddHealthChecks(); @@ -230,6 +232,7 @@ var app = builder.Build(); app.UseForwardedHeaders(); app.UseMiddleware<ExceptionMiddleware>(); +app.UseMiddleware<CookieAcceptanceMiddleware>(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment())