diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3b8c030..3a41091 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore src/Stott.Optimizely.RobotsHandler.sln - name: Build diff --git a/LICENSE.txt b/LICENSE.txt index bee4443..c9f1da4 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Mark Stott +Copyright (c) 2026 Mark Stott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Opal/Discovery.http b/Opal/Discovery.http index 8381816..9f18d9a 100644 --- a/Opal/Discovery.http +++ b/Opal/Discovery.http @@ -4,7 +4,7 @@ ### # Variables - Change these as needed -@baseUrl = https://localhost:44344 +@baseUrl = https://localhost:5000 @apiPath = /stott.robotshandler/opal # Test 1: Basic Discovery API call (IIS Express) diff --git a/Opal/Llms.http b/Opal/Llms.http index 4967f75..b618b13 100644 --- a/Opal/Llms.http +++ b/Opal/Llms.http @@ -5,10 +5,10 @@ ### # Variables - Change these as needed -@baseUrl = https://localhost:44344 +@baseUrl = https://localhost:5000 @apiPath = /stott.robotshandler/opal -@token = put-bearer-token-here -@testHostName = localhost:44344 +@token = X7hMZuo3idXCkgElP4iHrIdNnyY5WYNz +@testHostName = localhost:5000 # For production testing, uncomment these instead: # @baseUrl = https://test-cms.stott.pro @@ -50,7 +50,7 @@ Authorization: Bearer {{token}} { "parameters": { - "hostName": "example.com" + "hostName": "www.example.com" } } @@ -78,7 +78,7 @@ Authorization: Bearer {{token}} { "parameters": { - "llmsId": "d994532b-02df-42dd-93ef-18bee7c3346f", + "llmsId": "1b0349f2-c352-4d9f-b7d3-285389d02c53", "llmsTxtContent": "# LLMS.TXT\n\n# Model Information\nmodel-name: GPT-4\nmodel-version: 2024-03\ntraining-data-cutoff: 2023-04\n\n# Usage Guidelines\nallowed-uses: content-generation, code-assistance, research\nrestricted-uses: personal-data-processing, medical-diagnosis\n\n# Contact Information\ncontact: ai-team@example.com\nwebsite: https://example.com/ai-policy" } } diff --git a/Opal/Robots.http b/Opal/Robots.http index 1e06144..51f3901 100644 --- a/Opal/Robots.http +++ b/Opal/Robots.http @@ -4,11 +4,11 @@ ### # Variables - Change these as needed -@baseUrl = https://localhost:44344 +@baseUrl = https://localhost:5000 @apiPath = /stott.robotshandler/opal -@token = put-bearer-token-here -@testHostNameOne = localhost:44344 -@testHostNameTwo = localhost:44347 +@token = X7hMZuo3idXCkgElP4iHrIdNnyY5WYNzq +@testHostNameOne = localhost:5000 +@testHostNameTwo = localhost:5002 # For production testing, uncomment these instead: # @baseUrl = https://test-cms.stott.pro @@ -78,7 +78,7 @@ Authorization: Bearer {{token}} { "parameters": { - "robotsId": "3cd8ebcd-3733-4774-8c67-8bd28b440ca7", + "robotsId": "1a1543cd-78ce-4b38-9ae1-c63a4ff5c60e", "robotsTxtContent": "User-agent: *\nDisallow: /admin/\nDisallow: /private/\n\nSitemap: https://example.com/sitemap.xml" } } diff --git a/README.md b/README.md index d100217..ab16f5a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Stott.Optimizely.RobotsHandler [![Platform](https://img.shields.io/badge/Platform-.NET%206-blue.svg?style=flat)](https://docs.microsoft.com/en-us/dotnet/) -[![Platform](https://img.shields.io/badge/Optimizely-%2012-blue.svg?style=flat)](http://world.episerver.com/cms/) +[![Platform](https://img.shields.io/badge/Optimizely-%2012%2C13-blue.svg?style=flat)](http://world.episerver.com/cms/) [![GitHub](https://img.shields.io/github/license/GeekInTheNorth/Stott.Optimizely.RobotsHandler)](https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler/blob/main/LICENSE.txt) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/GeekInTheNorth/Stott.Optimizely.RobotsHandler/dotnet.yml?branch=main) ![Nuget](https://img.shields.io/nuget/v/Stott.Optimizely.RobotsHandler) -This is an admin extension for Optimizely CMS 12+ for managing robots content. Stott Robots Handler is a free to use module, however if you want to show your support, buy me a coffee on ko-fi: +This is an admin extension for Optimizely CMS 12 & 13 for managing robots content. Stott Robots Handler is a free to use module, however if you want to show your support, buy me a coffee on ko-fi: [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/V7V0RX2BQ) diff --git a/Sample/OptimizelyTwelveTest.sln b/Sample/OptimizelyTwelveTest.sln index 3aee915..802453c 100644 --- a/Sample/OptimizelyTwelveTest.sln +++ b/Sample/OptimizelyTwelveTest.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34928.147 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11512.155 d18.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OptimizelyTwelveTest", "OptimizelyTwelveTest\OptimizelyTwelveTest.csproj", "{5D9A020E-AE86-4902-AC8C-94C3A24297BF}" EndProject @@ -9,6 +9,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stott.Optimizely.RobotsHand EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stott.Optimizely.RobotsHandler.Test", "..\src\Stott.Optimizely.RobotsHandler.Test\Stott.Optimizely.RobotsHandler.Test.csproj", "{D7D6E33E-542E-46E1-B0C6-BFC4210E47D1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpalTests", "OpalTests", "{B229F2B1-7980-41EE-816E-59813F665803}" + ProjectSection(SolutionItems) = preProject + ..\Opal\Discovery.http = ..\Opal\Discovery.http + ..\Opal\Llms.http = ..\Opal\Llms.http + ..\Opal\Robots.http = ..\Opal\Robots.http + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Sample/OptimizelyTwelveTest/Features/Configuration/SetupMigrationStep.cs b/Sample/OptimizelyTwelveTest/Features/Configuration/SetupMigrationStep.cs new file mode 100644 index 0000000..a26b480 --- /dev/null +++ b/Sample/OptimizelyTwelveTest/Features/Configuration/SetupMigrationStep.cs @@ -0,0 +1,139 @@ +using System; +using System.Globalization; +using System.Linq; + +using EPiServer; +using EPiServer.Applications; +using EPiServer.Core; +using EPiServer.DataAbstraction.Migration; +using EPiServer.DataAccess; +using EPiServer.Security; +using EPiServer.ServiceLocation; + +using OptimizelyTwelveTest.Features.Home; +using OptimizelyTwelveTest.Features.Settings; + +namespace OptimizelyTwelveTest.Features.Configuration +{ + public class SetupMigrationStep : MigrationStep + { + public override void AddChanges() + { + try + { + var appRepository = ServiceLocator.Current.GetInstance(); + if (!ConfigurationExists(appRepository)) + { + var contentRepository = ServiceLocator.Current.GetInstance(); + SetUpSystem(appRepository, contentRepository, 1, 5000, 5001); + SetUpSystem(appRepository, contentRepository, 2, 5002, 5003); + SetUpHeadlessSystem(appRepository, contentRepository, 3, 5004, 5005); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error encountered during initial configuration: {ex.Message}"); + } + } + + private static bool ConfigurationExists(IApplicationRepository appRepository) + { + var sites = appRepository.List(); + return sites.Any(); + } + + private static void SetUpSystem(IApplicationRepository appRepository, IContentRepository contentRepository, int siteNumber, int primaryPort, int cmsPort) + { + var culture = new CultureInfo("en"); + + // Create HomePage + var newHomePage = contentRepository.GetDefault(ContentReference.RootPage, culture); + newHomePage.Name = $"Home {siteNumber}"; + newHomePage.Heading = $"Home {siteNumber}"; + newHomePage.MetaTitle = $"Home {siteNumber}"; + + var homePageReference = contentRepository.Save(newHomePage, SaveAction.Publish, AccessLevel.NoAccess); + + // Create SiteSettings + var newSiteSettings = contentRepository.GetDefault(homePageReference, culture); + newSiteSettings.Name = $"[Site Settings {siteNumber}]"; + newSiteSettings.SiteName = $"Site {siteNumber}"; + + var siteSettingsReference = contentRepository.Save(newSiteSettings, SaveAction.Publish, AccessLevel.NoAccess); + + // Update Home Page + var existingHomePage = contentRepository.Get(homePageReference, culture); + var editableHomePage = existingHomePage.CreateWritableClone() as HomePage; + editableHomePage.SiteSettings = siteSettingsReference.ToReferenceWithoutVersion(); + + homePageReference = contentRepository.Save(editableHomePage, SaveAction.Publish, AccessLevel.NoAccess); + + // Create Site + var newSite = new InProcessWebsite($"TestWebsite{siteNumber}", homePageReference.ToReferenceWithoutVersion()) + { + DisplayName = $"Test Website {siteNumber}" + }; + + newSite.Hosts.Add(new ApplicationHost($"localhost:{primaryPort}") + { + PreferredUrlScheme = UrlScheme.Https, + Type = ApplicationHostType.Primary + }); + + newSite.Hosts.Add(new ApplicationHost($"localhost:{cmsPort}") + { + PreferredUrlScheme = UrlScheme.Https, + Type = ApplicationHostType.Edit + }); + + appRepository.SaveAsync(newSite).GetAwaiter().GetResult(); + } + + private static void SetUpHeadlessSystem(IApplicationRepository appRepository, IContentRepository contentRepository, int siteNumber, int primaryPort, int previewPort) + { + var culture = new CultureInfo("en"); + + // Create HomePage + var newHomePage = contentRepository.GetDefault(ContentReference.RootPage, culture); + newHomePage.Name = $"Home {siteNumber}"; + newHomePage.Heading = $"Home {siteNumber}"; + newHomePage.MetaTitle = $"Home {siteNumber}"; + + var homePageReference = contentRepository.Save(newHomePage, SaveAction.Publish, AccessLevel.NoAccess); + + // Create SiteSettings + var newSiteSettings = contentRepository.GetDefault(homePageReference, culture); + newSiteSettings.Name = $"[Site Settings {siteNumber}]"; + newSiteSettings.SiteName = $"Site {siteNumber}"; + + var siteSettingsReference = contentRepository.Save(newSiteSettings, SaveAction.Publish, AccessLevel.NoAccess); + + // Update Home Page + var existingHomePage = contentRepository.Get(homePageReference, culture); + var editableHomePage = existingHomePage.CreateWritableClone() as HomePage; + editableHomePage.SiteSettings = siteSettingsReference.ToReferenceWithoutVersion(); + + homePageReference = contentRepository.Save(editableHomePage, SaveAction.Publish, AccessLevel.NoAccess); + + // Create Site + var newSite = new Website($"TestWebsite{siteNumber}", homePageReference.ToReferenceWithoutVersion()) + { + DisplayName = $"Test Website {siteNumber}" + }; + + newSite.Hosts.Add(new ApplicationHost($"localhost:{primaryPort}") + { + PreferredUrlScheme = UrlScheme.Https, + Type = ApplicationHostType.Primary + }); + + newSite.Hosts.Add(new ApplicationHost($"localhost:{previewPort}") + { + PreferredUrlScheme = UrlScheme.Https, + Type = ApplicationHostType.Preview + }); + + appRepository.SaveAsync(newSite).GetAwaiter().GetResult(); + } + } +} diff --git a/Sample/OptimizelyTwelveTest/Features/Home/HomePageController.cs b/Sample/OptimizelyTwelveTest/Features/Home/HomePageController.cs index cdd3a4d..73c2189 100644 --- a/Sample/OptimizelyTwelveTest/Features/Home/HomePageController.cs +++ b/Sample/OptimizelyTwelveTest/Features/Home/HomePageController.cs @@ -1,34 +1,17 @@ -namespace OptimizelyTwelveTest.Features.Home -{ - using System.Threading.Tasks; +namespace OptimizelyTwelveTest.Features.Home; - using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; - using OptimizelyTwelveTest.Features.Common; +using Microsoft.AspNetCore.Mvc; - using Stott.Security.Optimizely.Features.Csp.Settings.Service; +using OptimizelyTwelveTest.Features.Common; - public class HomePageController : PageControllerBase +public class HomePageController : PageControllerBase +{ + public async Task Index(HomePage currentPage) { - private readonly ICspSettingsService _cspSettingsService; - - public HomePageController(ICspSettingsService cspSettingsService) - { - _cspSettingsService = cspSettingsService; - } - - public async Task Index(HomePage currentPage, bool resetReportMode) - { - var model = new HomePageViewModel { CurrentPage = currentPage }; - - if (resetReportMode) - { - var currentSettings = await _cspSettingsService.GetAsync(); - currentSettings.IsReportOnly = true; - await _cspSettingsService.SaveAsync(currentSettings, "Mark Stott"); - } + var model = new HomePageViewModel { CurrentPage = currentPage }; - return View(model); - } + return View(model); } } diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchPage.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchPage.cs deleted file mode 100644 index abf7149..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchPage.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search -{ - using EPiServer.Core; - using EPiServer.DataAbstraction; - using EPiServer.DataAnnotations; - using EPiServer.Web; - - using OptimizelyTwelveTest.Features.Common.Pages; - - using System.ComponentModel.DataAnnotations; - - [ContentType( - DisplayName = "Search Page", - GUID = "529f8bf1-2b92-4993-9bf7-d7af1a82b464", - Description = "A page for general search of the site.", - GroupName = SystemTabNames.Content)] - public class SearchPage : SitePageData - { - [Display( - Name = "Hero Image", - Description = "The image to render at the top of the page", - GroupName = SystemTabNames.Content, - Order = 10)] - [UIHint(UIHint.Image)] - public virtual ContentReference HeroImage { get; set; } - - [Display( - Name = "Heading", - Description = "The H1 to display", - GroupName = SystemTabNames.Content, - Order = 20)] - public virtual string Heading { get; set; } - - [Display( - Name = "Initial Page Size", - Description = "The number of results to display on the initial search.", - GroupName = SystemTabNames.Content, - Order = 30)] - [Range(1,100)] - public virtual int InitialPageSize { get; set; } - - [Display( - Name = "Load More Page Size", - Description = "The number of results to display on subsequent page loads.", - GroupName = SystemTabNames.Content, - Order = 40)] - [Range(1, 100)] - public virtual int LoadMorePageSize { get; set; } - - [Display( - Name = "Load More CTA Text", - Description = "The text to display on the Load More CTA button.", - GroupName = SystemTabNames.Content, - Order = 50)] - [Required] - public virtual string LoadMoreCtaText { get; set; } - - public override void SetDefaultValues(ContentType contentType) - { - base.SetDefaultValues(contentType); - - this.InitialPageSize = 12; - this.LoadMorePageSize = 9; - this.LoadMoreCtaText = "Load More"; - } - } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchPageController.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchPageController.cs deleted file mode 100644 index eee7741..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchPageController.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search; - -using System.Threading.Tasks; - -using MediatR; - -using Microsoft.AspNetCore.Mvc; - -using Newtonsoft.Json; - -using OptimizelyTwelveTest.Features.Attributes; -using OptimizelyTwelveTest.Features.Common; - -public sealed class SearchPageController : PageControllerBase -{ - private readonly IMediator _mediator; - - public SearchPageController(IMediator mediator) - { - _mediator = mediator; - } - - [HttpGet] - public async Task Index(SearchPage currentContent, string searchText, int page = 1) - { - var query = new SearchQuery - { - InitialPageSize = currentContent.InitialPageSize, - LoadMorePageSize = currentContent.LoadMorePageSize, - Page = page, - SearchText = searchText - }; - var response = await _mediator.Send(query); - var model = new SearchPageViewModel - { - CurrentPage = currentContent, - SearchResults = response - }; - - return View(model); - } - - [HttpGet] - [DisallowRobotsActonFilter] - public async Task Search(SearchPage currentContent, string searchText, int page) - { - var query = new SearchQuery - { - InitialPageSize = currentContent.InitialPageSize, - LoadMorePageSize = currentContent.LoadMorePageSize, - Page = page, - SearchText = searchText - }; - var response = await _mediator.Send(query); - - return new ContentResult - { - Content = JsonConvert.SerializeObject(response), - ContentType = "application/json", - StatusCode = 200 - }; - } -} diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchPageViewModel.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchPageViewModel.cs deleted file mode 100644 index 6aa24fe..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchPageViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search -{ - using OptimizelyTwelveTest.Features.Common.Pages; - - public class SearchPageViewModel: ISitePageViewModel - { - public SearchPage CurrentPage { get; set; } - - public SearchResponse SearchResults { get; set; } - } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchQuery.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchQuery.cs deleted file mode 100644 index 19cb314..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search -{ - using MediatR; - - public class SearchQuery : IRequest - { - public string SearchText { get; set; } - - public int InitialPageSize { get; set; } - - public int LoadMorePageSize { get; set; } - - public int Page { get; set; } - } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchQueryHandler.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchQueryHandler.cs deleted file mode 100644 index 3b9d956..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchQueryHandler.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search; - -using System.Linq; -using EPiServer.Find; -using EPiServer.Find.Cms; - -using MediatR; - -using OptimizelyTwelveTest.Features.Common.Pages; - -using System.Threading; -using System.Threading.Tasks; -using EPiServer.Web.Routing; -using OptimizelyTwelveTest.Features.GeneralContent; -using OptimizelyTwelveTest.Features.Home; - -public sealed class SearchQueryHandler : IRequestHandler -{ - private readonly IClient _findClient; - - private readonly UrlResolver _urlResolver; - - public SearchQueryHandler(IClient findClient, UrlResolver urlResolver) - { - _findClient = findClient; - _urlResolver = urlResolver; - } - - public Task Handle(SearchQuery request, CancellationToken cancellationToken) - { - var pageSize = request.InitialPageSize; - var skip = 0; - if (request.Page > 1) - { - pageSize = request.LoadMorePageSize; - skip = request.InitialPageSize + (request.LoadMorePageSize * (request.Page - 1)); - } - - var searchResult = _findClient.Search() - .For(request.SearchText) - .UsingSynonyms() - .ApplyBestBets() - .Skip(skip) - .Take(pageSize) - .GetContentResult(); - - var response = new SearchResponse - { - TotalRecords = searchResult.TotalMatching, - Results = searchResult.Items.Select(ToSearchResultItem).ToList() - }; - - return Task.FromResult(response); - } - - private SearchResultItem ToSearchResultItem(SitePageData sitePageData) - { - if (sitePageData is HomePage homePage) - { - return new SearchResultItem - { - Title = homePage.TeaserTitle ?? homePage.Heading, - Description = homePage.TeaserText, - ImageUrl = _urlResolver.GetUrl(homePage.TeaserImage) - }; - } - - if (sitePageData is GeneralContentPage generalContentPage) - { - return new SearchResultItem - { - Title = generalContentPage.TeaserTitle ?? generalContentPage.Heading, - Description = generalContentPage.TeaserText, - ImageUrl = _urlResolver.GetUrl(generalContentPage.TeaserImage) - }; - } - - return new SearchResultItem - { - Title = sitePageData.TeaserTitle, - Description = sitePageData.TeaserText, - ImageUrl = _urlResolver.GetUrl(sitePageData.TeaserImage) - }; - } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchResponse.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchResponse.cs deleted file mode 100644 index 7e7021e..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search; - -using System.Collections.Generic; - -public sealed class SearchResponse -{ - public int TotalRecords { get; set; } - - public IList Results { get; set; } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Features/Search/SearchResultItem.cs b/Sample/OptimizelyTwelveTest/Features/Search/SearchResultItem.cs deleted file mode 100644 index 05bbc9f..0000000 --- a/Sample/OptimizelyTwelveTest/Features/Search/SearchResultItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OptimizelyTwelveTest.Features.Search -{ - public class SearchResultItem - { - public string Title { get; set; } - - public string Description { get; set; } - - public string ImageUrl { get; set; } - } -} \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/OptimizelyTwelveTest.csproj b/Sample/OptimizelyTwelveTest/OptimizelyTwelveTest.csproj index a3eec00..4507e42 100644 --- a/Sample/OptimizelyTwelveTest/OptimizelyTwelveTest.csproj +++ b/Sample/OptimizelyTwelveTest/OptimizelyTwelveTest.csproj @@ -1,14 +1,15 @@  - net6.0 + net10.0 - - - - - + + + + + + diff --git a/Sample/OptimizelyTwelveTest/Program.cs b/Sample/OptimizelyTwelveTest/Program.cs index 94c6c8e..c8d563d 100644 --- a/Sample/OptimizelyTwelveTest/Program.cs +++ b/Sample/OptimizelyTwelveTest/Program.cs @@ -1,23 +1,45 @@ namespace OptimizelyTwelveTest; +using System; + using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; public class Program { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); public static IHostBuilder CreateHostBuilder(string[] args) { - return Host.CreateDefaultBuilder(args) - .ConfigureCmsDefaults() - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.UseStaticWebAssets(); - }); + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + + if (environment == Environments.Development) + { + return Host.CreateDefaultBuilder(args) + .ConfigureCmsDefaults() + .ConfigureWebHostDefaults(webBuilder => webBuilder.ConfigureKestrel((context, options) => + { + // Add valid HTTPS ports for development that are compatible with Opti Id + options.ListenLocalhost(5000, options => { options.UseHttps(); }); + options.ListenLocalhost(5001, options => { options.UseHttps(); }); + options.ListenLocalhost(5002, options => { options.UseHttps(); }); + options.ListenLocalhost(5003, options => { options.UseHttps(); }); + + options.Limits.MaxRequestBodySize = 2147483648; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(60); + }) + .UseStartup()); + } + else + { + return Host.CreateDefaultBuilder(args) + .ConfigureCmsDefaults() + .ConfigureWebHostDefaults(webBuilder => webBuilder.ConfigureKestrel((context, options) => + { + options.Limits.MaxRequestBodySize = 2147483648; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(60); + }) + .UseStartup()); + } } } \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Properties/launchSettings.json b/Sample/OptimizelyTwelveTest/Properties/launchSettings.json index a9e9794..fc5d19d 100644 --- a/Sample/OptimizelyTwelveTest/Properties/launchSettings.json +++ b/Sample/OptimizelyTwelveTest/Properties/launchSettings.json @@ -1,27 +1,12 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:60396/", - "sslPort": 44344 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "OptimizelyTwelveTest": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "https://localhost:5000" } } } \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/ServiceExtensions/RedirectServiceExtensions.cs b/Sample/OptimizelyTwelveTest/ServiceExtensions/RedirectServiceExtensions.cs index c4b3fae..9715735 100644 --- a/Sample/OptimizelyTwelveTest/ServiceExtensions/RedirectServiceExtensions.cs +++ b/Sample/OptimizelyTwelveTest/ServiceExtensions/RedirectServiceExtensions.cs @@ -23,6 +23,7 @@ public static class RedirectServiceExtensions .Add(context => { if (context.HttpContext.Request.Path.StartsWithSegments("/util") || + context.HttpContext.Request.Path.StartsWithSegments("/optimizely") || context.HttpContext.Request.Path.StartsWithSegments("/EPiServer") || context.HttpContext.Request.Path.StartsWithSegments("/EPiServer.Forms") || context.HttpContext.Request.Path.StartsWithSegments("/episerver") || diff --git a/Sample/OptimizelyTwelveTest/Startup.cs b/Sample/OptimizelyTwelveTest/Startup.cs index afe6d6d..dfe07d6 100644 --- a/Sample/OptimizelyTwelveTest/Startup.cs +++ b/Sample/OptimizelyTwelveTest/Startup.cs @@ -1,40 +1,48 @@ namespace OptimizelyTwelveTest; using System; - +using EPiServer.Cms.Shell.UI; using EPiServer.Cms.UI.AspNetIdentity; +using EPiServer.Cms.UI.VisitorGroups; +using EPiServer.Data; +using EPiServer.DependencyInjection; using EPiServer.Scheduler; using EPiServer.Web.Routing; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Net.Http.Headers; - +using Optimizely.Graph.DependencyInjection; using OptimizelyTwelveTest.Features.Common; -using OptimizelyTwelveTest.Features.Search; using ServiceExtensions; using Stott.Optimizely.RobotsHandler.Common; using Stott.Optimizely.RobotsHandler.Configuration; -using Stott.Optimizely.RobotsHandler.Robots; -using Stott.Security.Optimizely.Common; -using Stott.Security.Optimizely.Features.Configuration; -public class Startup +public class Startup(IWebHostEnvironment webHostingEnvironment) { - private readonly IWebHostEnvironment _webHostingEnvironment; - - public Startup(IWebHostEnvironment webHostingEnvironment) - { - _webHostingEnvironment = webHostingEnvironment; - } - public void ConfigureServices(IServiceCollection services) { - if (_webHostingEnvironment.IsDevelopment()) + services.AddCms() + .AddCmsAspNetIdentity() + .AddAdminUserRegistration(options => { options.Behavior = RegisterAdminUserBehaviors.Enabled; }) + .AddVisitorGroupsMvc() + .AddVisitorGroupsUI() + .AddContentGraph() + .AddContentManager() + .AddCmsTagHelpers() + .AddCustomDependencies(); + + services.Configure(options => + { + options.UpdateDatabaseCompatibilityLevel = true; + }); + + if (webHostingEnvironment.IsDevelopment()) { services.Configure(o => { @@ -43,7 +51,6 @@ public void ConfigureServices(IServiceCollection services) } services.AddRazorPages(); - services.AddCmsAspNetIdentity(); // Various serialization formats. //// services.AddMvc().AddNewtonsoftJson(); @@ -52,36 +59,26 @@ public void ConfigureServices(IServiceCollection services) config.JsonSerializerOptions.PropertyNamingPolicy = new UpperCaseNamingPolicy(); }); - services.AddCms() - .AddFind() - .AddMediatR(config => - { - config.RegisterServicesFromAssemblyContaining(); - config.RegisterServicesFromAssemblyContaining(); - }) - .AddCustomDependencies() - .AddSwaggerGen(); - //// services.AddRobotsHandler(); services.AddRobotsHandler(authorizationOptions => { authorizationOptions.AddPolicy(RobotsConstants.AuthorizationPolicy, policy => { - policy.RequireRole("RobotAdmins","Everyone"); + policy.RequireRole("RobotAdmins", "WebAdmins", "Everyone"); }); }); - services.AddStottSecurity(cspSetupOptions => - { - cspSetupOptions.ConnectionStringName = "EPiServerDB"; - }, - authorizationOptions => - { - authorizationOptions.AddPolicy(CspConstants.AuthorizationPolicy, policy => - { - policy.RequireRole("WebAdmins"); - }); - }); + //services.AddStottSecurity(cspSetupOptions => + //{ + // cspSetupOptions.ConnectionStringName = "EPiServerDB"; + //}, + //authorizationOptions => + //{ + // authorizationOptions.AddPolicy(CspConstants.AuthorizationPolicy, policy => + // { + // policy.RequireRole("WebAdmins"); + // }); + //}); services.ConfigureApplicationCookie(options => { @@ -100,7 +97,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseResponseCaching(); app.Use(async (context, next) => { - if (context.Request is not null && !context.Request.Path.Value.StartsWith("/episerver/", StringComparison.OrdinalIgnoreCase)) + if (context.Request is not null && !context.Request.Path.Value.StartsWith("/optimizely/", StringComparison.OrdinalIgnoreCase)) { if (context.Response.Headers.ContainsKey(HeaderNames.CacheControl)) { @@ -108,7 +105,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } else { - context.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, max-age=0"); + context.Response.Headers.Append(HeaderNames.CacheControl, "no-cache, max-age=0"); } } @@ -121,16 +118,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseAuthorization(); - app.UseStottSecurity(); + // app.UseStottSecurity(); app.UseRobotsHandler(); - app.UseSwagger(); - app.UseSwaggerUI(); - app.UseEndpoints(endpoints => { endpoints.MapContent(); - endpoints.MapRazorPages(); + endpoints.MapControllers(); }); } } \ No newline at end of file diff --git a/Sample/OptimizelyTwelveTest/Views/CustomAdminPage/Index.cshtml b/Sample/OptimizelyTwelveTest/Views/CustomAdminPage/Index.cshtml index 769473b..7ad9011 100644 --- a/Sample/OptimizelyTwelveTest/Views/CustomAdminPage/Index.cshtml +++ b/Sample/OptimizelyTwelveTest/Views/CustomAdminPage/Index.cshtml @@ -1,14 +1,14 @@ -@using EPiServer.Shell.Navigation +@addTagHelper *, EPiServer.Shell.UI +@using EPiServer.Shell.Navigation @{ - Layout = string.Empty; + Layout = null; } Content Security Policy Management - @Html.CreatePlatformNavigationMenu() +

My Custom Admin Page diff --git a/Sample/OptimizelyTwelveTest/Views/SearchPage/Index.cshtml b/Sample/OptimizelyTwelveTest/Views/SearchPage/Index.cshtml deleted file mode 100644 index 2c0d8e5..0000000 --- a/Sample/OptimizelyTwelveTest/Views/SearchPage/Index.cshtml +++ /dev/null @@ -1,27 +0,0 @@ -@using EPiServer.Web.Mvc.Html -@model OptimizelyTwelveTest.Features.Search.SearchPageViewModel - -
- @Html.PropertyFor(x => x.CurrentPage.HeroImage) -

@Html.PropertyFor(x => x.CurrentPage.Heading)

-
- @if (Model.SearchResults != null && Model.SearchResults.Results != null) - { - - - - - - - - @foreach (var sitePage in Model.SearchResults.Results) - { - - - - - } -
TitleTeaser Text
@sitePage.Title@sitePage.Description
- } -
-
diff --git a/Sample/OptimizelyTwelveTest/Views/Shared/_Layout.cshtml b/Sample/OptimizelyTwelveTest/Views/Shared/_Layout.cshtml index 77f5612..6d6e663 100644 --- a/Sample/OptimizelyTwelveTest/Views/Shared/_Layout.cshtml +++ b/Sample/OptimizelyTwelveTest/Views/Shared/_Layout.cshtml @@ -1,6 +1,5 @@ @using EPiServer.Web.Mvc.Html @using OptimizelyTwelveTest.Features.Common.Pages -@using Stott.Security.Optimizely.Features.Csp.Reporting; @model ISitePageViewModel diff --git a/Sample/OptimizelyTwelveTest/appsettings.json b/Sample/OptimizelyTwelveTest/appsettings.json index 3aec503..bbd9029 100644 --- a/Sample/OptimizelyTwelveTest/appsettings.json +++ b/Sample/OptimizelyTwelveTest/appsettings.json @@ -1,49 +1,44 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning", - "EPiServer": "Warning", - "Microsoft.Hosting.Lifetime": "Warning" - } - }, - "urls": "http://*:8000/;https://*:8001/;", - "AllowedHosts": "*", + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "EPiServer": "Warning", + "Microsoft.Hosting.Lifetime": "Warning" + } + }, + "AllowedHosts": "*", "ConnectionStrings": { - "EPiServerDB": "Server=(localdb)\\mssqllocaldb;Database=StottRobotsLocalDb;Trusted_Connection=true;MultipleActiveResultSets=true" + "EPiServerDB": "Server=(localdb)\\mssqllocaldb;Database=StottRobotsCms13Db;Trusted_Connection=true;MultipleActiveResultSets=true" }, - "EPiServer": { - "Find": { - "ServiceUrl": "https://demo01.find.episerver.net/RXQGZ5QpXU9cuRSN2181hqA77ZFrUq2e/", - "DefaultIndex": "markstott_cmstwelvedev" - }, - "Cms": { - "DataAccess": { - "Retries": "5", - "RetryDelay": "0:0:0.1", - "DatabaseQueryTimeout": "0:0:30", - "DatabaseMode": "ReadWrite" - }, - "DynamicDataStore": { - "AutoResolveTypes": "true", - "AutoRemapStores": "true", - "DeleteAllOperationTimeout": "600" - }, - "MappedRoles": { - "Items": { - "CmsEditors": { - "MappedRoles": [ - "WebEditors", - "WebAdmins" - ] - }, - "CmsAdmins": { - "MappedRoles": [ - "WebAdmins" - ] - } - } - } + "EPiServer": { + "Cms": { + "DataAccess": { + "Retries": "5", + "RetryDelay": "0:0:0.1", + "DatabaseQueryTimeout": "0:0:30", + "DatabaseMode": "ReadWrite" + }, + "DynamicDataStore": { + "AutoResolveTypes": "true", + "AutoRemapStores": "true", + "DeleteAllOperationTimeout": "600" + }, + "MappedRoles": { + "Items": { + "CmsEditors": { + "MappedRoles": [ + "WebEditors", + "WebAdmins" + ] + }, + "CmsAdmins": { + "MappedRoles": [ + "WebAdmins" + ] + } } + } } + } } diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/CMS/CMS.zip b/Sample/OptimizelyTwelveTest/modules/_protected/CMS/CMS.zip index 9755dd7..de39450 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/CMS/CMS.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/CMS/CMS.zip differ diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.TinyMce/EPiServer.Cms.TinyMce.zip b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.TinyMce/EPiServer.Cms.TinyMce.zip index 66e0120..4beaf1a 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.TinyMce/EPiServer.Cms.TinyMce.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.TinyMce/EPiServer.Cms.TinyMce.zip differ diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Admin/EPiServer.Cms.UI.Admin.zip b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Admin/EPiServer.Cms.UI.Admin.zip index 828f834..0d89d02 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Admin/EPiServer.Cms.UI.Admin.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Admin/EPiServer.Cms.UI.Admin.zip differ diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Settings/EPiServer.Cms.UI.Settings.zip b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Settings/EPiServer.Cms.UI.Settings.zip index 63ddf91..5f0356b 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Settings/EPiServer.Cms.UI.Settings.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.Settings/EPiServer.Cms.UI.Settings.zip differ diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.VisitorGroups/EPiServer.Cms.UI.VisitorGroups.zip b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.VisitorGroups/EPiServer.Cms.UI.VisitorGroups.zip index 8f9d973..bfbdbde 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.VisitorGroups/EPiServer.Cms.UI.VisitorGroups.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/EPiServer.Cms.UI.VisitorGroups/EPiServer.Cms.UI.VisitorGroups.zip differ diff --git a/Sample/OptimizelyTwelveTest/modules/_protected/Shell/Shell.zip b/Sample/OptimizelyTwelveTest/modules/_protected/Shell/Shell.zip index 86adaae..4bfa0fe 100644 Binary files a/Sample/OptimizelyTwelveTest/modules/_protected/Shell/Shell.zip and b/Sample/OptimizelyTwelveTest/modules/_protected/Shell/Shell.zip differ diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Extensions/SiteDefinitionExtensionTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Extensions/SiteDefinitionExtensionTests.cs index 789a2a2..8eb614c 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Extensions/SiteDefinitionExtensionTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Extensions/SiteDefinitionExtensionTests.cs @@ -1,88 +1,82 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; - -using EPiServer.Web; +using EPiServer.Applications; using NUnit.Framework; - -using Stott.Optimizely.RobotsHandler.Extensions; +using Stott.Optimizely.RobotsHandler.Applications; namespace Stott.Optimizely.RobotsHandler.Test.Extensions; [TestFixture] -public sealed class SiteDefinitionExtensionTests +public sealed class ApplicationMapperTests { [Test] - public void ToHostSummaries_ReturnsDefaultHost_WhenHostDefinitionsIsNull() + public void ToHostSummaries_ReturnsEmpty_WhenHostDefinitionsIsNull() { // Arrange - IList hostDefinitions = null; + IList hostDefinitions = null; // Act - var result = hostDefinitions.ToHostSummaries().ToList(); + var result = ApplicationMapper.CreateHostSummaries(hostDefinitions).ToList(); // Assert - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result.First().DisplayName, Is.EqualTo("Default")); - Assert.That(result.First().HostName, Is.EqualTo(string.Empty)); + Assert.That(result, Is.Empty); } [Test] - public void ToHostSummaries_ReturnsDefaultHost_WhenHostDefinitionsIsEmpty() + public void ToHostSummaries_ReturnsEmpty_WhenHostDefinitionsIsEmpty() { // Arrange - IList hostDefinitions = new List(); + IList hostDefinitions = new List(); // Act - var result = hostDefinitions.ToHostSummaries().ToList(); + var result = ApplicationMapper.CreateHostSummaries(hostDefinitions).ToList(); // Assert - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result.First().DisplayName, Is.EqualTo("Default")); - Assert.That(result.First().HostName, Is.EqualTo(string.Empty)); + Assert.That(result, Is.Empty); } [Test] public void ToHostSummaries_ReturnsDefaultAndHosts_WhenHostDefinitionsIsNotEmpty() { // Arrange - IList hostDefinitions = new List + IList hostDefinitions = new List { - new() { Name = "host1.com" }, - new() { Name = "host2.com" } + new ApplicationHost("host1.com"), + new ApplicationHost("host2.com") }; // Act - var result = hostDefinitions.ToHostSummaries().ToList(); + var result = ApplicationMapper.CreateHostSummaries(hostDefinitions).ToList(); // Assert Assert.That(result, Has.Count.EqualTo(3)); Assert.That(result[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].DisplayName, Is.EqualTo("host1.com")); + Assert.That(result[1].DisplayName, Is.EqualTo("http://host1.com/")); Assert.That(result[1].HostName, Is.EqualTo("host1.com")); - Assert.That(result[2].DisplayName, Is.EqualTo("host2.com")); + Assert.That(result[2].DisplayName, Is.EqualTo("http://host2.com/")); Assert.That(result[2].HostName, Is.EqualTo("host2.com")); } [Test] - public void ToHostSummaries_ExcludesHostsWithoutUrl() + public void ToHostSummaries_IncludesAllHostsWithUrls() { // Arrange - IList hostDefinitions = new List + IList hostDefinitions = new List { - new() { Name = "host1.com" }, - new() { Name = "*" } + new ApplicationHost("host1.com"), + new ApplicationHost("host2.com") }; // Act - var result = hostDefinitions.ToHostSummaries().ToList(); + var result = ApplicationMapper.CreateHostSummaries(hostDefinitions).ToList(); // Assert - Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result, Has.Count.EqualTo(3)); Assert.That(result[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].DisplayName, Is.EqualTo("host1.com")); + Assert.That(result[1].DisplayName, Is.EqualTo("http://host1.com/")); Assert.That(result[1].HostName, Is.EqualTo("host1.com")); } } diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Llms/DefaultLlmsContentServiceTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Llms/DefaultLlmsContentServiceTests.cs index 7c955cc..1bbf4b4 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Llms/DefaultLlmsContentServiceTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Llms/DefaultLlmsContentServiceTests.cs @@ -1,13 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Text; -using EPiServer.Web; - using Moq; using NUnit.Framework; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Llms; using Stott.Optimizely.RobotsHandler.Models; @@ -16,11 +14,7 @@ namespace Stott.Optimizely.RobotsHandler.Test.Llms; [TestFixture] public class DefaultLlmsContentServiceTests { - private Mock _mockHostDefinition; - - private Mock _mockSiteDefinition; - - private Mock _mockSiteDefinitionRepository; + private Mock _mockApplicationDefinitionRepository; private Mock _mockRepository; @@ -29,29 +23,24 @@ public class DefaultLlmsContentServiceTests [SetUp] public void SetUp() { - _mockHostDefinition = new Mock(); - - _mockSiteDefinition = new Mock(); - _mockSiteDefinition.Setup(x => x.Hosts).Returns(new List { _mockHostDefinition.Object }); - - _mockSiteDefinitionRepository = new Mock(); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(new List { _mockSiteDefinition.Object }); + _mockApplicationDefinitionRepository = new Mock(); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync([]); _mockRepository = new Mock(); - _service = new DefaultLlmsContentService(_mockSiteDefinitionRepository.Object, _mockRepository.Object); + _service = new DefaultLlmsContentService(_mockApplicationDefinitionRepository.Object, _mockRepository.Object); } [Test] public void GetLlmsContent_siteId_ReturnsNullForAValidSiteWhenLlmsContentDoesNotExist() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; _mockRepository.Setup(x => x.Get(It.IsAny())).Returns((LlmsTxtEntity)null); - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List(0)); + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([]); // Act - var LlmsContent = _service.GetLlmsContent(siteId, null); + var LlmsContent = _service.GetLlmsContent(appId, null); // Assert Assert.That(LlmsContent, Is.Null); @@ -61,13 +50,13 @@ public void GetLlmsContent_siteId_ReturnsNullForAValidSiteWhenLlmsContentDoesNot public void GetLlmsContent_siteId_ReturnsLlmsContentForASpecificHostWhenLlmsContentExists() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; var LlmsContent = GetSavedLlms(); - var LlmsTxtEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", LlmsContent = LlmsContent }; - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { LlmsTxtEntity }); + var LlmsTxtEntity = new LlmsTxtEntity { AppId = appId, SpecificHost = "www.example.com", LlmsContent = LlmsContent }; + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([LlmsTxtEntity]); // Act - var result = _service.GetLlmsContent(siteId, "www.example.com"); + var result = _service.GetLlmsContent(appId, "www.example.com"); // Assert Assert.That(result, Is.EqualTo(LlmsContent)); @@ -77,13 +66,13 @@ public void GetLlmsContent_siteId_ReturnsLlmsContentForASpecificHostWhenLlmsCont public void GetLlmsContent_siteId_ReturnsDefaultRobotsForASiteWhenLlmsContentExistsButNoSpecificHostIsProvided() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; var LlmsContent = GetSavedLlms(); - var LlmsTxtEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = string.Empty, LlmsContent = LlmsContent }; - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { LlmsTxtEntity }); + var LlmsTxtEntity = new LlmsTxtEntity { AppId = appId, SpecificHost = string.Empty, LlmsContent = LlmsContent }; + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([LlmsTxtEntity]); // Act - var result = _service.GetLlmsContent(siteId, null); + var result = _service.GetLlmsContent(appId, null); // Assert Assert.That(result, Is.EqualTo(LlmsContent)); @@ -93,13 +82,13 @@ public void GetLlmsContent_siteId_ReturnsDefaultRobotsForASiteWhenLlmsContentExi public void GetLlmsContent_siteId_ReturnsNullForASiteWhenLlmsContentExistsButNoMatchingSpecificHostIsProvided() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; var LlmsContent = GetSavedLlms(); - var LlmsTxtEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", LlmsContent = LlmsContent }; - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { LlmsTxtEntity }); + var LlmsTxtEntity = new LlmsTxtEntity { AppId = appId, SpecificHost = "www.example.com", LlmsContent = LlmsContent }; + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([LlmsTxtEntity]); // Act - var result = _service.GetLlmsContent(siteId, "www.non-matching.com"); + var result = _service.GetLlmsContent(appId, "www.non-matching.com"); // Assert Assert.That(result, Is.Null); @@ -109,14 +98,14 @@ public void GetLlmsContent_siteId_ReturnsNullForASiteWhenLlmsContentExistsButNoM public void GetLlmsContent_siteId_ReturnsMatchingHostWhenBothDefaultAndDefinedHostExist() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; var LlmsContent = GetSavedLlms(); - var hostDefinedEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", LlmsContent = "Defined Robots" }; - var defaultEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, LlmsContent = "Default Robots" }; - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { hostDefinedEntity, defaultEntity }); + var hostDefinedEntity = new LlmsTxtEntity { AppId = appId, SpecificHost = "www.example.com", LlmsContent = "Defined Robots" }; + var defaultEntity = new LlmsTxtEntity { AppId = appId, LlmsContent = "Default Robots" }; + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([hostDefinedEntity, defaultEntity]); // Act - var result = _service.GetLlmsContent(siteId, "www.example.com"); + var result = _service.GetLlmsContent(appId, "www.example.com"); // Assert Assert.That(result, Is.EqualTo(hostDefinedEntity.LlmsContent)); @@ -126,14 +115,14 @@ public void GetLlmsContent_siteId_ReturnsMatchingHostWhenBothDefaultAndDefinedHo public void GetLlmsContent_siteId_ReturnsDefaultHostWhenBothDefaultAndDefinedHostExistForANonMatchingHost() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; var LlmsContent = GetSavedLlms(); - var hostDefinedEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", LlmsContent = "Defined Robots" }; - var defaultEntity = new LlmsTxtEntity { Id = Guid.NewGuid(), SiteId = siteId, LlmsContent = "Default Robots" }; - _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { hostDefinedEntity, defaultEntity }); + var hostDefinedEntity = new LlmsTxtEntity { AppId = appId, SpecificHost = "www.example.com", LlmsContent = "Defined Robots" }; + var defaultEntity = new LlmsTxtEntity { AppId = appId, LlmsContent = "Default Robots" }; + _mockRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([hostDefinedEntity, defaultEntity]); // Act - var result = _service.GetLlmsContent(siteId, "www.non-matching.com"); + var result = _service.GetLlmsContent(appId, "www.non-matching.com"); // Assert Assert.That(result, Is.EqualTo(defaultEntity.LlmsContent)); @@ -146,10 +135,12 @@ public void SaveLlmsContent_ThrowsArgumentExceptionWhenPassedAnEmptyGuid() var model = new SaveLlmsModel { Id = Guid.NewGuid(), - SiteId = Guid.Empty, + AppId = "appId", LlmsContent = GetSavedLlms() }; + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); + // Assert Assert.Throws(() => _service.Save(model)); } @@ -161,11 +152,11 @@ public void SaveLlmsContent_ThrowsArgumentExceptionWhenSiteIdRefersToANonExistan var model = new SaveLlmsModel { Id = Guid.NewGuid(), - SiteId = Guid.NewGuid(), + AppId = "appId", LlmsContent = GetSavedLlms() }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns((SiteDefinition)null); + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert Assert.Throws(() => _service.Save(model)); @@ -178,11 +169,11 @@ public void SaveLlmsContent_CallsSaveOnTheLlmsContentRepositoryForAValidSite() var model = new SaveLlmsModel { Id = Guid.NewGuid(), - SiteId = Guid.NewGuid(), + AppId = "appId", LlmsContent = GetSavedLlms() }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns(_mockSiteDefinition.Object); + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync(new ApplicationViewModel { AppId = "appId" }); // Act _service.Save(model); @@ -231,15 +222,15 @@ public void DoesConflictExists_WhenPassedAnEmptyGuid_ThenFalseIsReturned() } [Test] - [TestCase("f70719d4-adc6-4a06-8662-7c7e78ab3dbc", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", false)] - [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", false)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", true)] - [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", true)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.non-matching.com", false)] - public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfiguration_ThenTrueIsReturned(Guid id, Guid siteId, string host, bool expectedValue) + [TestCase("f70719d4-adc6-4a06-8662-7c7e78ab3dbc", "appId1", "", false)] + [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "appId1", "", true)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "appId1", "", true)] + [TestCase("00000000-0000-0000-0000-000000000000", "appId1", "", true)] + [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "appId1", "www.example.com", false)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "appId1", "www.example.com", true)] + [TestCase("00000000-0000-0000-0000-000000000000", "appId1", "www.example.com", true)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "appId1", "www.non-matching.com", false)] + public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfiguration_ThenTrueIsReturned(string id, string appId, string host, bool expectedValue) { // Arrange var savedRecords = new List @@ -247,20 +238,20 @@ public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfigurati new() { Id = Guid.Parse("f70719d4-adc6-4a06-8662-7c7e78ab3dbc"), - SiteId = Guid.Parse("5645bc86-f7f7-4c3a-924c-13612c55914a"), + AppId = "appId1", SpecificHost = string.Empty, LlmsContent = GetSavedLlms() }, new() { Id = Guid.Parse("a841af98-cdbd-4e64-82b2-f31f3b0fe647"), - SiteId = Guid.Parse("5645bc86-f7f7-4c3a-924c-13612c55914a"), + AppId = "appId1", SpecificHost = "www.example.com", LlmsContent = GetSavedLlms() } }; - var model = new SaveLlmsModel { Id = id, SiteId = siteId, SpecificHost = host, LlmsContent = GetSavedLlms() }; + var model = new SaveLlmsModel { Id = Guid.Parse(id), AppId = appId, SpecificHost = host, LlmsContent = GetSavedLlms() }; _mockRepository.Setup(x => x.GetAll()).Returns(savedRecords); // Act @@ -274,40 +265,40 @@ public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfigurati public void GetDefault_WhenPassedAnInvalidSiteId_ThenThrowsArgumentException() { // Arrange - var siteId = Guid.Empty; + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert - Assert.Throws(() => _service.GetDefault(siteId)); + Assert.Throws(() => _service.GetDefault(string.Empty)); } [Test] public void GetDefault_WhenPassedAValidSiteId_ButTheRepositoryDoesNotContainTheSite_ThenThrowsArgumentException() { // Arrange - var siteId = Guid.NewGuid(); + var appId = "appId"; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns((SiteDefinition)null); + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert - Assert.Throws(() => _service.GetDefault(siteId)); + Assert.Throws(() => _service.GetDefault(appId)); } [Test] public void GetDefault_WhenPassedAValidSiteId_ThenReturnsAValidSiteRobotsViewModel() { // Arrange - var siteId = Guid.NewGuid(); - var mockSiteDefinition = new SiteDefinition { Id = siteId }; + var appId = "appId"; + var application = new ApplicationViewModel { AppId = appId }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns(mockSiteDefinition); + _mockApplicationDefinitionRepository.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync(application); // Act - var result = _service.GetDefault(siteId); + var result = _service.GetDefault(appId); // Assert Assert.That(result, Is.Not.Null); Assert.That(result.Id, Is.EqualTo(Guid.Empty)); - Assert.That(result.SiteId, Is.EqualTo(siteId)); + Assert.That(result.AppId, Is.EqualTo(appId)); Assert.That(result.SpecificHost, Is.Null); } @@ -315,14 +306,14 @@ public void GetDefault_WhenPassedAValidSiteId_ThenReturnsAValidSiteRobotsViewMod public void GetAll_WhenTheLlmsContentRepositoryHasNoRecords_ThenAnEmptyCollectionShouldBeReturned() { // Arrange - var sites = new List + var sites = new List { - new() { Id = Guid.NewGuid() }, - new() { Id = Guid.NewGuid() } + new() { AppId = "appId1" }, + new() { AppId = "appId2" } }; - _mockRepository.Setup(x => x.GetAll()).Returns(new List(0)); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockRepository.Setup(x => x.GetAll()).Returns([]); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(sites); // Act var result = _service.GetAll(); @@ -336,20 +327,21 @@ public void GetAll_WhenTheLlmsContentRepositoryHasNoRecords_ThenAnEmptyCollectio public void GetAll_WhenTheLlmsContentRepositoryHasRecords_ThenAnEntryIsReturnedForEachItem() { // Arrange - var sites = new List + var defaultHosts = new List { new() { DisplayName = "Default", HostName = string.Empty } }; + var sites = new List { - new() { Id = Guid.NewGuid() }, - new() { Id = Guid.NewGuid() } + new() { AppId = "appId1", AvailableHosts = defaultHosts }, + new() { AppId = "appId2", AvailableHosts = defaultHosts } }; var robotsEntities = new List { - new() { Id = Guid.NewGuid(), SiteId = sites[0].Id }, - new() { Id = Guid.NewGuid(), SiteId = sites[1].Id } + new() { AppId = sites[0].AppId }, + new() { AppId = sites[1].AppId } }; _mockRepository.Setup(x => x.GetAll()).Returns(robotsEntities); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(sites); // Act var result = _service.GetAll(); @@ -357,10 +349,10 @@ public void GetAll_WhenTheLlmsContentRepositoryHasRecords_ThenAnEntryIsReturnedF // Assert Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result[0].SiteId, Is.EqualTo(sites[0].Id)); + Assert.That(result[0].AppId, Is.EqualTo(sites[0].AppId)); Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].SiteId, Is.EqualTo(sites[1].Id)); + Assert.That(result[1].AppId, Is.EqualTo(sites[1].AppId)); Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); } @@ -369,20 +361,20 @@ public void GetAll_WhenTheLlmsContentRepositoryHasRecords_ThenAnEntryIsReturnedF public void GetAll_WhenThereAreLlmsContentForSpecificHosts_ThenAnEntryIsReturnedForEachItem() { // Arrange - var sites = new List + var sites = new List { - new() { Id = Guid.NewGuid(), Name = "Site 1", Hosts = new List { new() { Name = "www.exampleone.com" } } }, - new() { Id = Guid.NewGuid(), Name = "Site 2", Hosts = new List { new() { Name = "www.exampletwo.com" } } }, + new() { AppId = "appId1", AppName = "Site 1", AvailableHosts = [new() { DisplayName = "www.exampleone.com", HostName = "www.exampleone.com" }] }, + new() { AppId = "appId2", AppName = "Site 2", AvailableHosts = [new() { DisplayName = "www.exampletwo.com", HostName = "www.exampletwo.com" }] }, }; var robotsEntities = new List { - new() { Id = Guid.NewGuid(), SiteId = sites[0].Id, SpecificHost = "www.exampleone.com" }, - new() { Id = Guid.NewGuid(), SiteId = sites[1].Id, SpecificHost = "www.exampletwo.com" } + new() { AppId = sites[0].AppId, SpecificHost = "www.exampleone.com" }, + new() { AppId = sites[1].AppId, SpecificHost = "www.exampletwo.com" } }; _mockRepository.Setup(x => x.GetAll()).Returns(robotsEntities); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(sites); // Act var result = _service.GetAll(); @@ -391,21 +383,17 @@ public void GetAll_WhenThereAreLlmsContentForSpecificHosts_ThenAnEntryIsReturned Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result[0].SiteId, Is.EqualTo(sites[0].Id)); - Assert.That(result[0].SiteName, Is.EqualTo(sites[0].Name)); + Assert.That(result[0].AppId, Is.EqualTo(sites[0].AppId)); + Assert.That(result[0].AppName, Is.EqualTo(sites[0].AppName)); Assert.That(result[0].SpecificHost, Is.EqualTo("www.exampleone.com")); - Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[0].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[0].AvailableHosts[1].HostName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[1].SiteId, Is.EqualTo(sites[1].Id)); - Assert.That(result[1].SiteName, Is.EqualTo(sites[1].Name)); + Assert.That(result[1].AppId, Is.EqualTo(sites[1].AppId)); + Assert.That(result[1].AppName, Is.EqualTo(sites[1].AppName)); Assert.That(result[1].SpecificHost, Is.EqualTo("www.exampletwo.com")); - Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampletwo.com")); - Assert.That(result[1].AvailableHosts[1].HostName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo("www.exampletwo.com")); } [Test] @@ -424,8 +412,9 @@ public void Get_WhenPassedAnIdForALlmsTxtEntityThatDoesExistButForANonMatchingSi { // Arrange var id = Guid.NewGuid(); - var LlmsTxtEntity = new LlmsTxtEntity { Id = id, SiteId = Guid.NewGuid() }; + var LlmsTxtEntity = new LlmsTxtEntity { AppId = "nonMatchingAppId" }; _mockRepository.Setup(x => x.Get(It.IsAny())).Returns(LlmsTxtEntity); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync([]); // Assert Assert.Throws(() => _service.Get(id)); @@ -436,18 +425,20 @@ public void Get_WhenPassedAnIdForALlmsTxtEntityThatExists_ThenReturnsTheModel() { // Arrange var id = Guid.NewGuid(); - var siteId = Guid.NewGuid(); - var LlmsTxtEntity = new LlmsTxtEntity { Id = id, SiteId = siteId }; - _mockRepository.Setup(x => x.Get(It.IsAny())).Returns(LlmsTxtEntity); - - _mockSiteDefinition.Setup(x => x.Id).Returns(siteId); + var appId = "appId"; + var llmsTxtEntity = new LlmsTxtEntity { AppId = appId }; + _mockRepository.Setup(x => x.Get(It.IsAny())).Returns(llmsTxtEntity); + _mockApplicationDefinitionRepository.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync( + [ + new() { AppId = appId } + ]); // Act var result = _service.Get(id); // Assert Assert.That(result, Is.Not.Null); - Assert.That(result.Id, Is.EqualTo(id)); + Assert.That(result.Id, Is.EqualTo(llmsTxtEntity.Id.ExternalId)); } private static string GetSavedLlms() diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsApiControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsApiControllerTests.cs index 34e2968..b81b6fb 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsApiControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsApiControllerTests.cs @@ -42,7 +42,7 @@ public void ApiList_RetrievesDataFromTheRepository() } [Test] - [TestCaseSource(typeof(CommonTestCases), nameof(CommonTestCases.InvalidGuidStrings))] + [TestCaseSource(typeof(CommonTestCases), nameof(CommonTestCases.EmptyStrings))] public void Details_ThrowsArgumentExceptionWhenPresentedWithAnInvalidSiteId(string siteId) { // Assert @@ -72,7 +72,7 @@ public void Details_RetrievesDefaultModelWhenPresentedWithAnEmptyId() _controller.Details(Guid.Empty.ToString(), siteId); // Assert - _mockService.Verify(x => x.GetDefault(It.IsAny()), Times.Once); + _mockService.Verify(x => x.GetDefault(It.IsAny()), Times.Once); } [Test] diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsTextControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsTextControllerTests.cs index 8b7cf4e..6fb0f7e 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsTextControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Llms/LlmsTextControllerTests.cs @@ -1,4 +1,6 @@ -using System; +using System.Threading.Tasks; + +using EPiServer.Applications; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,8 @@ public class LlmsTextControllerTests { private LlmsTextController _controller; + private Mock _mockApplicationResolver; + private Mock _serviceMock; private Mock _mockHttpRequest; @@ -30,6 +34,7 @@ public class LlmsTextControllerTests [SetUp] public void SetUp() { + _mockApplicationResolver = new Mock(); _serviceMock = new Mock(); _loggerMock = new Mock>(); @@ -37,12 +42,13 @@ public void SetUp() _mockHttpRequest.Setup(x => x.Host).Returns(new HostString("www.example.com")); _mockHttpResponse = new Mock(); + _mockHttpResponse.Setup(x => x.Headers).Returns(new HeaderDictionary()); _mockHttpContext = new Mock(); _mockHttpContext.Setup(x => x.Request).Returns(_mockHttpRequest.Object); _mockHttpContext.Setup(x => x.Response).Returns(_mockHttpResponse.Object); - _controller = new LlmsTextController(_serviceMock.Object, _loggerMock.Object) + _controller = new LlmsTextController(_mockApplicationResolver.Object, _serviceMock.Object, _loggerMock.Object) { ControllerContext = new ControllerContext { @@ -52,14 +58,14 @@ public void SetUp() } [Test] - public void Index_WhenCalled_ReturnsRobotsContent() + public async Task Index_WhenCalled_ReturnsRobotsContent() { // Arrange var robotsContent = "User-agent: *\nDisallow: /"; - _serviceMock.Setup(x => x.GetLlmsContent(It.IsAny(), It.IsAny())).Returns(robotsContent); + _serviceMock.Setup(x => x.GetLlmsContent(It.IsAny(), It.IsAny())).Returns(robotsContent); // Act - var result = _controller.Index(); + var result = await _controller.Index(); // Assert var contentResult = result as ContentResult; diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalLlmsApiControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalLlmsApiControllerTests.cs index c21e0fe..4198e9e 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalLlmsApiControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalLlmsApiControllerTests.cs @@ -8,11 +8,10 @@ using Moq; using NUnit.Framework; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Llms; using Stott.Optimizely.RobotsHandler.Opal; using Stott.Optimizely.RobotsHandler.Opal.Models; -using Stott.Optimizely.RobotsHandler.Sites; using Stott.Optimizely.RobotsHandler.Test.TestCases; namespace Stott.Optimizely.RobotsHandler.Test.Opal; @@ -56,7 +55,7 @@ public void GetLlmsTxtConfigurations_WhenCalled_InvokesGetAllOnService() public void GetLlmsTxtConfigurations_GivenThereAreNoConfigurations_ThenAnEmptyArrayIsReturned() { // Arrange - _mockService.Setup(s => s.GetAll()).Returns(new List()); + _mockService.Setup(s => s.GetAll()).Returns(new List()); // Act var result = _controller.GetLlmsTxtConfigurations(new ToolRequest()) as ContentResult; @@ -280,7 +279,7 @@ public void SaveLlmsTxtConfigurations_GivenValidLlmsId_ThenConfigurationIsUpdate var llmsId = existingConfig.Id; var newContent = "User-agent: GPT\nDisallow: /admin"; - _mockService.Setup(s => s.GetAll()).Returns(new List { existingConfig }); + _mockService.Setup(s => s.GetAll()).Returns(new List { existingConfig }); var model = new ToolRequest { @@ -302,8 +301,8 @@ public void SaveLlmsTxtConfigurations_GivenValidLlmsId_ThenConfigurationIsUpdate _mockService.Verify(s => s.Save(It.Is(m => m.Id == llmsId && m.LlmsContent == newContent && - m.SiteName == existingConfig.SiteName && - m.SiteId == existingConfig.SiteId && + m.AppName == existingConfig.AppName && + m.AppId == existingConfig.AppId && m.SpecificHost == existingConfig.SpecificHost)), Times.Once); } @@ -551,36 +550,34 @@ public void SaveLlmsTxtConfigurations_WhenExceptionOccurs_ThenExceptionIsRethrow Times.Once); } - private static List CreateDummyData() + private static List CreateDummyData() { - return new List - { - new SiteLlmsViewModel - { + return + [ + new ApplicationLlmsViewModel { Id = Guid.NewGuid(), - SiteName = "Test One", + AppName = "Test One", SpecificHost = "specific.test", - AvailableHosts = new List - { - new SiteHostViewModel { HostName = "available1.test" }, - new SiteHostViewModel { HostName = "available2.test" } - }, + AvailableHosts = + [ + new HostViewModel { HostName = "available1.test", DisplayName = "Site One" }, + new HostViewModel { HostName = "available2.test", DisplayName = "Site Two" } + ], LlmsContent = "User-agent: *\nDisallow: /", IsForWholeSite = false }, - new SiteLlmsViewModel - { + new ApplicationLlmsViewModel { Id = Guid.NewGuid(), - SiteName = "Test Two", + AppName = "Test Two", SpecificHost = null, - AvailableHosts = new List - { - new SiteHostViewModel { HostName = "available3.test" }, - new SiteHostViewModel { HostName = "available4.test" } - }, + AvailableHosts = + [ + new HostViewModel { HostName = "available3.test", DisplayName = "Site Three" }, + new HostViewModel { HostName = "available4.test", DisplayName = "Site Four" } + ], LlmsContent = "User-agent: *\nDisallow: /private", IsForWholeSite = true } - }; + ]; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalRobotsApiControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalRobotsApiControllerTests.cs index e80f3ee..edf5753 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalRobotsApiControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Opal/OpalRobotsApiControllerTests.cs @@ -8,11 +8,10 @@ using Moq; using NUnit.Framework; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Opal; using Stott.Optimizely.RobotsHandler.Opal.Models; using Stott.Optimizely.RobotsHandler.Robots; -using Stott.Optimizely.RobotsHandler.Sites; using Stott.Optimizely.RobotsHandler.Test.TestCases; namespace Stott.Optimizely.RobotsHandler.Test.Opal; @@ -302,8 +301,8 @@ public void SaveRobotTxtConfigurations_GivenValidRobotsId_ThenConfigurationIsUpd _mockService.Verify(s => s.Save(It.Is(m => m.Id == robotsId && m.RobotsContent == newContent && - m.SiteName == existingConfig.SiteName && - m.SiteId == existingConfig.SiteId && + m.AppName == existingConfig.AppName && + m.AppId == existingConfig.AppId && m.SpecificHost == existingConfig.SpecificHost)), Times.Once); } @@ -522,34 +521,34 @@ public void SaveRobotTxtConfigurations_WhenExceptionOccurs_ThenExceptionIsRethro private static List CreateDummyData() { - return new List - { + return + [ new SiteRobotsViewModel { Id = Guid.NewGuid(), - SiteName = "Test One", + AppName = "Test One", SpecificHost = "specific.test", - AvailableHosts = new List - { - new SiteHostViewModel { HostName = "available1.test" }, - new SiteHostViewModel { HostName = "available2.test" } - }, + AvailableHosts = + [ + new HostViewModel { HostName = "available1.test", DisplayName = "Site 1" }, + new HostViewModel { HostName = "available2.test", DisplayName = "Site 2" } + ], RobotsContent = "User-agent: *\nDisallow: /admin", IsForWholeSite = false }, new SiteRobotsViewModel { Id = Guid.NewGuid(), - SiteName = "Test Two", + AppName = "Test Two", SpecificHost = null, - AvailableHosts = new List - { - new SiteHostViewModel { HostName = "available3.test" }, - new SiteHostViewModel { HostName = "available4.test" } - }, + AvailableHosts = + [ + new HostViewModel { HostName = "available3.test", DisplayName = "Site 3" }, + new HostViewModel { HostName = "available4.test", DisplayName = "Site 4" } + ], RobotsContent = "User-agent: *\nDisallow: /private", IsForWholeSite = true } - }; + ]; } } diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsApiControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsApiControllerTests.cs index f5419fc..88a4ac0 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsApiControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsApiControllerTests.cs @@ -42,7 +42,7 @@ public void ApiList_RetrievesDataFromTheRepository() } [Test] - [TestCaseSource(typeof(CommonTestCases), nameof(CommonTestCases.InvalidGuidStrings))] + [TestCaseSource(typeof(CommonTestCases), nameof(CommonTestCases.EmptyStrings))] public void Details_ThrowsArgumentExceptionWhenPresentedWithAnInvalidSiteId(string siteId) { // Assert @@ -72,7 +72,7 @@ public void Details_RetrievesDefaultModelWhenPresentedWithAnEmptyId() _controller.Details(Guid.Empty.ToString(), siteId); // Assert - _mockService.Verify(x => x.GetDefault(It.IsAny()), Times.Once); + _mockService.Verify(x => x.GetDefault(It.IsAny()), Times.Once); } [Test] diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsContentServiceTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsContentServiceTests.cs index 410e271..faed8b5 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsContentServiceTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsContentServiceTests.cs @@ -1,13 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Text; -using EPiServer.Web; - using Moq; using NUnit.Framework; +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Models; using Stott.Optimizely.RobotsHandler.Robots; @@ -16,11 +15,7 @@ namespace Stott.Optimizely.RobotsHandler.Test.Robots; [TestFixture] public sealed class RobotsContentServiceTests { - private Mock _mockHostDefinition; - - private Mock _mockSiteDefinition; - - private Mock _mockSiteDefinitionRepository; + private Mock _mockAppService; private Mock _mockRobotsContentRepository; @@ -29,29 +24,24 @@ public sealed class RobotsContentServiceTests [SetUp] public void SetUp() { - _mockHostDefinition = new Mock(); - - _mockSiteDefinition = new Mock(); - _mockSiteDefinition.Setup(x => x.Hosts).Returns(new List { _mockHostDefinition.Object }); - - _mockSiteDefinitionRepository = new Mock(); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(new List { _mockSiteDefinition.Object }); + _mockAppService = new Mock(); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync([]); _mockRobotsContentRepository = new Mock(); - _robotsContentService = new RobotsContentService(_mockSiteDefinitionRepository.Object, _mockRobotsContentRepository.Object); + _robotsContentService = new RobotsContentService(_mockAppService.Object, _mockRobotsContentRepository.Object); } [Test] public void GetRobotsContent_siteId_ReturnsDefaultRobotsForAValidSiteWhenRobotsContentDoesNotExist() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); _mockRobotsContentRepository.Setup(x => x.Get(It.IsAny())).Returns((RobotsEntity)null); - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List(0)); + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([]); // Act - var robotsContent = _robotsContentService.GetRobotsContent(siteId, null); + var robotsContent = _robotsContentService.GetRobotsContent(appId, null); // Assert Assert.That(robotsContent, Is.EqualTo(_robotsContentService.GetDefaultRobotsContent())); @@ -61,13 +51,13 @@ public void GetRobotsContent_siteId_ReturnsDefaultRobotsForAValidSiteWhenRobotsC public void GetRobotsContent_siteId_ReturnsRobotsContentForASpecificHostWhenRobotsContentExists() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); var robotsContent = GetSavedRobots(); - var robotsEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", RobotsContent = robotsContent }; - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { robotsEntity }); + var robotsEntity = new RobotsEntity { AppId = appId, SpecificHost = "www.example.com", RobotsContent = robotsContent }; + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([robotsEntity]); // Act - var result = _robotsContentService.GetRobotsContent(siteId, "www.example.com"); + var result = _robotsContentService.GetRobotsContent(appId, "www.example.com"); // Assert Assert.That(result, Is.EqualTo(robotsContent)); @@ -77,13 +67,13 @@ public void GetRobotsContent_siteId_ReturnsRobotsContentForASpecificHostWhenRobo public void GetRobotsContent_siteId_ReturnsDefaultRobotsForASiteWhenRobotsContentExistsButNoSpecificHostIsProvided() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); var robotsContent = GetSavedRobots(); - var robotsEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = string.Empty, RobotsContent = robotsContent }; - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { robotsEntity }); + var robotsEntity = new RobotsEntity { AppId = appId, SpecificHost = string.Empty, RobotsContent = robotsContent }; + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([robotsEntity]); // Act - var result = _robotsContentService.GetRobotsContent(siteId, null); + var result = _robotsContentService.GetRobotsContent(appId, null); // Assert Assert.That(result, Is.EqualTo(robotsContent)); @@ -93,13 +83,13 @@ public void GetRobotsContent_siteId_ReturnsDefaultRobotsForASiteWhenRobotsConten public void GetRobotsContent_siteId_ReturnsDefaultRobotsForASiteWhenRobotsContentExistsButNoMatchingSpecificHostIsProvided() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); var robotsContent = GetSavedRobots(); - var robotsEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", RobotsContent = robotsContent }; - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { robotsEntity }); + var robotsEntity = new RobotsEntity { AppId = appId, SpecificHost = "www.example.com", RobotsContent = robotsContent }; + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([robotsEntity]); // Act - var result = _robotsContentService.GetRobotsContent(siteId, "www.non-matching.com"); + var result = _robotsContentService.GetRobotsContent(appId, "www.non-matching.com"); // Assert Assert.That(result, Is.EqualTo(_robotsContentService.GetDefaultRobotsContent())); @@ -109,14 +99,14 @@ public void GetRobotsContent_siteId_ReturnsDefaultRobotsForASiteWhenRobotsConten public void GetRobotsContent_siteId_ReturnsMatchingHostWhenBothDefaultAndDefinedHostExist() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); var robotsContent = GetSavedRobots(); - var hostDefinedEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", RobotsContent = "Defined Robots" }; - var defaultEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, RobotsContent = "Default Robots" }; - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { hostDefinedEntity, defaultEntity }); + var hostDefinedEntity = new RobotsEntity { AppId = appId, SpecificHost = "www.example.com", RobotsContent = "Defined Robots" }; + var defaultEntity = new RobotsEntity { AppId = appId, RobotsContent = "Default Robots" }; + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([hostDefinedEntity, defaultEntity]); // Act - var result = _robotsContentService.GetRobotsContent(siteId, "www.example.com"); + var result = _robotsContentService.GetRobotsContent(appId, "www.example.com"); // Assert Assert.That(result, Is.EqualTo(hostDefinedEntity.RobotsContent)); @@ -126,14 +116,14 @@ public void GetRobotsContent_siteId_ReturnsMatchingHostWhenBothDefaultAndDefined public void GetRobotsContent_siteId_ReturnsDefaultHostWhenBothDefaultAndDefinedHostExistForANonMatchingHost() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); var robotsContent = GetSavedRobots(); - var hostDefinedEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, SpecificHost = "www.example.com", RobotsContent = "Defined Robots" }; - var defaultEntity = new RobotsEntity { Id = Guid.NewGuid(), SiteId = siteId, RobotsContent = "Default Robots" }; - _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns(new List { hostDefinedEntity, defaultEntity }); + var hostDefinedEntity = new RobotsEntity { AppId = appId, SpecificHost = "www.example.com", RobotsContent = "Defined Robots" }; + var defaultEntity = new RobotsEntity { AppId = appId, RobotsContent = "Default Robots" }; + _mockRobotsContentRepository.Setup(x => x.GetAllForSite(It.IsAny())).Returns([hostDefinedEntity, defaultEntity]); // Act - var result = _robotsContentService.GetRobotsContent(siteId, "www.non-matching.com"); + var result = _robotsContentService.GetRobotsContent(appId, "www.non-matching.com"); // Assert Assert.That(result, Is.EqualTo(defaultEntity.RobotsContent)); @@ -146,10 +136,12 @@ public void SaveRobotsContent_ThrowsArgumentExceptionWhenPassedAnEmptyGuid() var model = new SaveRobotsModel { Id = Guid.NewGuid(), - SiteId = Guid.Empty, + AppId = null, RobotsContent = GetSavedRobots() }; + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); + // Assert Assert.Throws(() => _robotsContentService.Save(model)); } @@ -161,11 +153,11 @@ public void SaveRobotsContent_ThrowsArgumentExceptionWhenSiteIdRefersToANonExist var model = new SaveRobotsModel { Id = Guid.NewGuid(), - SiteId = Guid.NewGuid(), + AppId = "some-app-id", RobotsContent = GetSavedRobots() }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns((SiteDefinition)null); + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert Assert.Throws(() => _robotsContentService.Save(model)); @@ -178,11 +170,11 @@ public void SaveRobotsContent_CallsSaveOnTheRobotsContentRepositoryForAValidSite var model = new SaveRobotsModel { Id = Guid.NewGuid(), - SiteId = Guid.NewGuid(), + AppId = "some-app-id", RobotsContent = GetSavedRobots() }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns(_mockSiteDefinition.Object); + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync(new ApplicationViewModel { AppId = "some-app-id" }); // Act _robotsContentService.Save(model); @@ -231,15 +223,15 @@ public void DoesConflictExists_WhenPassedAnEmptyGuid_ThenFalseIsReturned() } [Test] - [TestCase("f70719d4-adc6-4a06-8662-7c7e78ab3dbc", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", false)] - [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86-f7f7-4c3a-924c-13612c55914a", "", true)] - [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", false)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", true)] - [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.example.com", true)] - [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86-f7f7-4c3a-924c-13612c55914a", "www.non-matching.com", false)] - public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfiguration_ThenTrueIsReturned(Guid id, Guid siteId, string host, bool expectedValue) + [TestCase("f70719d4-adc6-4a06-8662-7c7e78ab3dbc", "5645bc86", "", false)] + [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86", "", true)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86", "", true)] + [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86", "", true)] + [TestCase("a841af98-cdbd-4e64-82b2-f31f3b0fe647", "5645bc86", "www.example.com", false)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86", "www.example.com", true)] + [TestCase("00000000-0000-0000-0000-000000000000", "5645bc86", "www.example.com", true)] + [TestCase("db107c5e-73ff-442f-93f3-bd99f56603f5", "5645bc86", "www.non-matching.com", false)] + public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfiguration_ThenTrueIsReturned(string id, string appId, string host, bool expectedValue) { // Arrange var savedRecords = new List @@ -247,20 +239,20 @@ public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfigurati new() { Id = Guid.Parse("f70719d4-adc6-4a06-8662-7c7e78ab3dbc"), - SiteId = Guid.Parse("5645bc86-f7f7-4c3a-924c-13612c55914a"), + AppId = "5645bc86", SpecificHost = string.Empty, RobotsContent = GetSavedRobots() }, new() { Id = Guid.Parse("a841af98-cdbd-4e64-82b2-f31f3b0fe647"), - SiteId = Guid.Parse("5645bc86-f7f7-4c3a-924c-13612c55914a"), + AppId = "5645bc86", SpecificHost = "www.example.com", RobotsContent = GetSavedRobots() } }; - var model = new SaveRobotsModel { Id = id, SiteId = siteId, SpecificHost = host, RobotsContent = GetSavedRobots() }; + var model = new SaveRobotsModel { Id = Guid.Parse(id), AppId = appId, SpecificHost = host, RobotsContent = GetSavedRobots() }; _mockRobotsContentRepository.Setup(x => x.GetAll()).Returns(savedRecords); // Act @@ -271,43 +263,43 @@ public void DoesConflictExists_GivenTheRepositoryContainsAConflictingConfigurati } [Test] - public void GetDefault_WhenPassedAnInvalidSiteId_ThenThrowsArgumentException() + public void GetDefault_WhenPassedAnInvalidAppId_ThenThrowsArgumentException() { // Arrange - var siteId = Guid.Empty; + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert - Assert.Throws(() => _robotsContentService.GetDefault(siteId)); + Assert.Throws(() => _robotsContentService.GetDefault(null)); } [Test] - public void GetDefault_WhenPassedAValidSiteId_ButTheRepositoryDoesNotContainTheSite_ThenThrowsArgumentException() + public void GetDefault_WhenPassedAValidAppId_ButTheServiceDoesNotContainTheApplication_ThenThrowsArgumentException() { // Arrange - var siteId = Guid.NewGuid(); + var appId = Guid.NewGuid().ToString(); - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns((SiteDefinition)null); + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync((ApplicationViewModel)null); // Assert - Assert.Throws(() => _robotsContentService.GetDefault(siteId)); + Assert.Throws(() => _robotsContentService.GetDefault(appId)); } [Test] - public void GetDefault_WhenPassedAValidSiteId_ThenReturnsAValidSiteRobotsViewModel() + public void GetDefault_WhenPassedAValidAppId_ThenReturnsAValidSiteRobotsViewModel() { // Arrange - var siteId = Guid.NewGuid(); - var mockSiteDefinition = new SiteDefinition { Id = siteId }; + var appId = Guid.NewGuid().ToString(); + var application = new ApplicationViewModel { AppId = appId }; - _mockSiteDefinitionRepository.Setup(x => x.Get(It.IsAny())).Returns(mockSiteDefinition); + _mockAppService.Setup(x => x.GetApplicationByIdAsync(It.IsAny())).ReturnsAsync(application); // Act - var result = _robotsContentService.GetDefault(siteId); + var result = _robotsContentService.GetDefault(appId); // Assert Assert.That(result, Is.Not.Null); Assert.That(result.Id, Is.EqualTo(Guid.Empty)); - Assert.That(result.SiteId, Is.EqualTo(siteId)); + Assert.That(result.AppId, Is.EqualTo(appId)); Assert.That(result.SpecificHost, Is.Null); } @@ -315,14 +307,15 @@ public void GetDefault_WhenPassedAValidSiteId_ThenReturnsAValidSiteRobotsViewMod public void GetAll_WhenTheRobotsContentRepositoryHasNoRecords_ThenDefaultRecordsShouldBeReturnedForEachSite() { // Arrange - var sites = new List + var defaultHosts = new List { new() { DisplayName = "Default", HostName = string.Empty } }; + var applications = new List { - new() { Id = Guid.NewGuid() }, - new() { Id = Guid.NewGuid() } + new() { AppId = Guid.NewGuid().ToString(), AvailableHosts = defaultHosts }, + new() { AppId = Guid.NewGuid().ToString(), AvailableHosts = defaultHosts } }; - _mockRobotsContentRepository.Setup(x => x.GetAll()).Returns(new List(0)); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockRobotsContentRepository.Setup(x => x.GetAll()).Returns([]); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(applications); // Act var result = _robotsContentService.GetAll(); @@ -330,10 +323,10 @@ public void GetAll_WhenTheRobotsContentRepositoryHasNoRecords_ThenDefaultRecords // Assert Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result[0].SiteId, Is.EqualTo(sites[0].Id)); + Assert.That(result[0].AppId, Is.EqualTo(applications[0].AppId)); Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].SiteId, Is.EqualTo(sites[1].Id)); + Assert.That(result[1].AppId, Is.EqualTo(applications[1].AppId)); Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); } @@ -342,20 +335,21 @@ public void GetAll_WhenTheRobotsContentRepositoryHasNoRecords_ThenDefaultRecords public void GetAll_WhenTheRobotsContentRepositoryHasRecords_ThenTheRecordsShouldBeReturnedForEachSite() { // Arrange - var sites = new List + var defaultHosts = new List { new() { DisplayName = "Default", HostName = string.Empty } }; + var applications = new List { - new() { Id = Guid.NewGuid() }, - new() { Id = Guid.NewGuid() } + new() { AppId = Guid.NewGuid().ToString(), AvailableHosts = defaultHosts }, + new() { AppId = Guid.NewGuid().ToString(), AvailableHosts = defaultHosts } }; var robotsEntities = new List { - new() { Id = Guid.NewGuid(), SiteId = sites[0].Id }, - new() { Id = Guid.NewGuid(), SiteId = sites[1].Id } + new() { AppId = applications[0].AppId }, + new() { AppId = applications[1].AppId } }; _mockRobotsContentRepository.Setup(x => x.GetAll()).Returns(robotsEntities); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(applications); // Act var result = _robotsContentService.GetAll(); @@ -363,10 +357,10 @@ public void GetAll_WhenTheRobotsContentRepositoryHasRecords_ThenTheRecordsShould // Assert Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result[0].SiteId, Is.EqualTo(sites[0].Id)); + Assert.That(result[0].AppId, Is.EqualTo(applications[0].AppId)); Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].SiteId, Is.EqualTo(sites[1].Id)); + Assert.That(result[1].AppId, Is.EqualTo(applications[1].AppId)); Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); } @@ -375,20 +369,20 @@ public void GetAll_WhenTheRobotsContentRepositoryHasRecords_ThenTheRecordsShould public void GetAll_WhenThereAreRobotsContentForSpecificHosts_ThenTheSpecificHostsShouldBeReturned() { // Arrange - var sites = new List + var applications = new List { - new() { Id = Guid.NewGuid(), Name = "Site 1", Hosts = new List { new() { Name = "www.exampleone.com" } } }, - new() { Id = Guid.NewGuid(), Name = "Site 2", Hosts = new List { new() { Name = "www.exampletwo.com" } } }, + new() { AppId = Guid.NewGuid().ToString(), AppName = "Site 1", AvailableHosts = [new() { DisplayName = "www.exampleone.com", HostName = "www.exampleone.com" }] }, + new() { AppId = Guid.NewGuid().ToString(), AppName = "Site 2", AvailableHosts = [new() { DisplayName = "www.exampletwo.com", HostName = "www.exampletwo.com" }] }, }; var robotsEntities = new List { - new() { Id = Guid.NewGuid(), SiteId = sites[0].Id, SpecificHost = "www.exampleone.com" }, - new() { Id = Guid.NewGuid(), SiteId = sites[1].Id, SpecificHost = "www.exampletwo.com" } + new() { AppId = applications[0].AppId, SpecificHost = "www.exampleone.com" }, + new() { AppId = applications[1].AppId, SpecificHost = "www.exampletwo.com" } }; _mockRobotsContentRepository.Setup(x => x.GetAll()).Returns(robotsEntities); - _mockSiteDefinitionRepository.Setup(x => x.List()).Returns(sites); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync(applications); // Act var result = _robotsContentService.GetAll(); @@ -397,41 +391,33 @@ public void GetAll_WhenThereAreRobotsContentForSpecificHosts_ThenTheSpecificHost Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(4)); - Assert.That(result[0].SiteId, Is.EqualTo(sites[0].Id)); - Assert.That(result[0].SiteName, Is.EqualTo(sites[0].Name)); + Assert.That(result[0].AppId, Is.EqualTo(applications[0].AppId)); + Assert.That(result[0].AppName, Is.EqualTo(applications[0].AppName)); Assert.That(result[0].SpecificHost, Is.Null); Assert.That(result[0].CanDelete, Is.False); - Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[0].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[0].AvailableHosts[1].HostName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[0].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[0].AvailableHosts[0].HostName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[1].SiteId, Is.EqualTo(sites[0].Id)); - Assert.That(result[1].SiteName, Is.EqualTo(sites[0].Name)); + Assert.That(result[1].AppId, Is.EqualTo(applications[0].AppId)); + Assert.That(result[1].AppName, Is.EqualTo(applications[0].AppName)); Assert.That(result[1].SpecificHost, Is.EqualTo("www.exampleone.com")); Assert.That(result[1].CanDelete, Is.True); - Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[1].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[1].AvailableHosts[1].HostName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[1].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampleone.com")); + Assert.That(result[1].AvailableHosts[0].HostName, Is.EqualTo("www.exampleone.com")); - Assert.That(result[2].SiteId, Is.EqualTo(sites[1].Id)); - Assert.That(result[2].SiteName, Is.EqualTo(sites[1].Name)); + Assert.That(result[2].AppId, Is.EqualTo(applications[1].AppId)); + Assert.That(result[2].AppName, Is.EqualTo(applications[1].AppName)); Assert.That(result[2].SpecificHost, Is.Null); Assert.That(result[2].CanDelete, Is.False); - Assert.That(result[2].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[2].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[2].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampletwo.com")); - Assert.That(result[2].AvailableHosts[1].HostName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[2].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[2].AvailableHosts[0].HostName, Is.EqualTo("www.exampletwo.com")); - Assert.That(result[3].SiteId, Is.EqualTo(sites[1].Id)); - Assert.That(result[3].SiteName, Is.EqualTo(sites[1].Name)); + Assert.That(result[3].AppId, Is.EqualTo(applications[1].AppId)); + Assert.That(result[3].AppName, Is.EqualTo(applications[1].AppName)); Assert.That(result[3].SpecificHost, Is.EqualTo("www.exampletwo.com")); Assert.That(result[3].CanDelete, Is.True); - Assert.That(result[3].AvailableHosts[0].DisplayName, Is.EqualTo("Default")); - Assert.That(result[3].AvailableHosts[0].HostName, Is.EqualTo(string.Empty)); - Assert.That(result[3].AvailableHosts[1].DisplayName, Is.EqualTo("www.exampletwo.com")); - Assert.That(result[3].AvailableHosts[1].HostName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[3].AvailableHosts[0].DisplayName, Is.EqualTo("www.exampletwo.com")); + Assert.That(result[3].AvailableHosts[0].HostName, Is.EqualTo("www.exampletwo.com")); } [Test] @@ -450,8 +436,9 @@ public void Get_WhenPassedAnIdForARobotsEntityThatDoesExistButForANonMatchingSit { // Arrange var id = Guid.NewGuid(); - var robotsEntity = new RobotsEntity { Id = id, SiteId = Guid.NewGuid() }; + var robotsEntity = new RobotsEntity { AppId = Guid.NewGuid().ToString() }; _mockRobotsContentRepository.Setup(x => x.Get(It.IsAny())).Returns(robotsEntity); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync([]); // Assert Assert.Throws(() => _robotsContentService.Get(id)); @@ -462,18 +449,17 @@ public void Get_WhenPassedAnIdForARobotsEntityThatExists_ThenReturnsTheModel() { // Arrange var id = Guid.NewGuid(); - var siteId = Guid.NewGuid(); - var robotsEntity = new RobotsEntity { Id = id, SiteId = siteId }; + var appId = Guid.NewGuid().ToString(); + var robotsEntity = new RobotsEntity { AppId = appId }; _mockRobotsContentRepository.Setup(x => x.Get(It.IsAny())).Returns(robotsEntity); - - _mockSiteDefinition.Setup(x => x.Id).Returns(siteId); + _mockAppService.Setup(x => x.GetAllApplicationsAsync()).ReturnsAsync([new() { AppId = appId }]); // Act var result = _robotsContentService.Get(id); // Assert Assert.That(result, Is.Not.Null); - Assert.That(result.Id, Is.EqualTo(id)); + Assert.That(result.Id, Is.EqualTo(robotsEntity.Id.ExternalId)); } private static string GetSavedRobots() @@ -485,4 +471,4 @@ private static string GetSavedRobots() return stringBuilder.ToString(); } -} \ No newline at end of file +} diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsTextControllerTests.cs b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsTextControllerTests.cs index 2956e5b..5211f3a 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsTextControllerTests.cs +++ b/src/Stott.Optimizely.RobotsHandler.Test/Robots/RobotsTextControllerTests.cs @@ -1,4 +1,6 @@ -using System; +using System.Threading.Tasks; + +using EPiServer.Applications; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,8 @@ public sealed class RobotsTextControllerTests { private RobotsTextController _controller; + private Mock _mockApplicationResolver; + private Mock _serviceMock; private Mock _mockHttpRequest; @@ -30,6 +34,7 @@ public sealed class RobotsTextControllerTests [SetUp] public void SetUp() { + _mockApplicationResolver = new Mock(); _serviceMock = new Mock(); _loggerMock = new Mock>(); @@ -37,12 +42,13 @@ public void SetUp() _mockHttpRequest.Setup(x => x.Host).Returns(new HostString("www.example.com")); _mockHttpResponse = new Mock(); + _mockHttpResponse.Setup(x => x.Headers).Returns(new HeaderDictionary()); _mockHttpContext = new Mock(); _mockHttpContext.Setup(x => x.Request).Returns(_mockHttpRequest.Object); _mockHttpContext.Setup(x => x.Response).Returns(_mockHttpResponse.Object); - _controller = new RobotsTextController(_serviceMock.Object, _loggerMock.Object) + _controller = new RobotsTextController(_mockApplicationResolver.Object, _serviceMock.Object, _loggerMock.Object) { ControllerContext = new ControllerContext { @@ -52,14 +58,14 @@ public void SetUp() } [Test] - public void Index_WhenCalled_ReturnsRobotsContent() + public async Task Index_WhenCalled_ReturnsRobotsContent() { // Arrange var robotsContent = "User-agent: *\nDisallow: /"; - _serviceMock.Setup(x => x.GetRobotsContent(It.IsAny(), It.IsAny())).Returns(robotsContent); + _serviceMock.Setup(x => x.GetRobotsContent(It.IsAny(), It.IsAny())).Returns(robotsContent); // Act - var result = _controller.Index(); + var result = await _controller.Index(); // Assert var contentResult = result as ContentResult; @@ -68,4 +74,4 @@ public void Index_WhenCalled_ReturnsRobotsContent() Assert.That(contentResult.ContentType, Is.EqualTo("text/plain")); Assert.That(contentResult.StatusCode, Is.EqualTo(200)); } -} \ No newline at end of file +} diff --git a/src/Stott.Optimizely.RobotsHandler.Test/Stott.Optimizely.RobotsHandler.Test.csproj b/src/Stott.Optimizely.RobotsHandler.Test/Stott.Optimizely.RobotsHandler.Test.csproj index 1342f13..e99ddc0 100644 --- a/src/Stott.Optimizely.RobotsHandler.Test/Stott.Optimizely.RobotsHandler.Test.csproj +++ b/src/Stott.Optimizely.RobotsHandler.Test/Stott.Optimizely.RobotsHandler.Test.csproj @@ -1,16 +1,16 @@  - net6.0 + net10.0 false - - - - + + + + diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/.env b/src/Stott.Optimizely.RobotsHandler.Ui/.env index 0dafb94..5f6fdc9 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/.env +++ b/src/Stott.Optimizely.RobotsHandler.Ui/.env @@ -6,7 +6,7 @@ VITE_APP_LLMS_LIST='https://localhost:44344/stott.robotshandler/api/llms/list/' VITE_APP_LLMS_EDIT='https://localhost:44344/stott.robotshandler/api/llms/details/' VITE_APP_LLMS_SAVE='https://localhost:44344/stott.robotshandler/api/llms/save/' VITE_APP_LLMS_DELETE='https://localhost:44344/stott.robotshandler/api/llms/delete/' -VITE_APP_SITES_LIST='https://localhost:44344/stott.robotshandler/api/sites/' +VITE_APP_APPLICATIONS_LIST='https://localhost:44344/stott.robotshandler/api/applications/' VITE_APP_ENVIRONMENT_LIST='https://localhost:44344/stott.robotshandler/api/environment/list/' VITE_APP_ENVIRONMENT_SAVE='https://localhost:44344/stott.robotshandler/api/environment/save/' VITE_APP_OPALTOKEN_LIST='https://localhost:44344/stott.robotshandler/api/opal-tokens/list/' diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/.env.production b/src/Stott.Optimizely.RobotsHandler.Ui/.env.production index 91e75a4..dd1d897 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/.env.production +++ b/src/Stott.Optimizely.RobotsHandler.Ui/.env.production @@ -6,7 +6,7 @@ VITE_APP_LLMS_LIST='/stott.robotshandler/api/llms/list/' VITE_APP_LLMS_EDIT='/stott.robotshandler/api/llms/details/' VITE_APP_LLMS_SAVE='/stott.robotshandler/api/llms/save/' VITE_APP_LLMS_DELETE='/stott.robotshandler/api/llms/delete/' -VITE_APP_SITES_LIST='/stott.robotshandler/api/sites/' +VITE_APP_APPLICATIONS_LIST='/stott.robotshandler/api/applications/' VITE_APP_ENVIRONMENT_LIST='/stott.robotshandler/api/environment/list/' VITE_APP_ENVIRONMENT_SAVE='/stott.robotshandler/api/environment/save/' VITE_APP_OPALTOKEN_LIST='/stott.robotshandler/api/opal-tokens/list/' diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/package-lock.json b/src/Stott.Optimizely.RobotsHandler.Ui/package-lock.json index 94802f6..a0ea5ad 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/package-lock.json +++ b/src/Stott.Optimizely.RobotsHandler.Ui/package-lock.json @@ -57,6 +57,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1045,6 +1046,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1499,6 +1501,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1555,6 +1558,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1871,6 +1875,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -2485,6 +2490,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4150,6 +4156,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4251,6 +4258,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4291,6 +4299,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5149,6 +5158,7 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteLlms.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteLlms.jsx index 0e461f7..299931c 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteLlms.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteLlms.jsx @@ -5,10 +5,10 @@ import { Alert, Button, Modal } from 'react-bootstrap' function AddSiteLlms(props) { const [showModal, setShowModal] = useState(false); - const [siteCollection, setSiteCollection] = useState([]); + const [appCollection, setAppCollection] = useState([]); const [hostCollection, setHostCollection] = useState([]); - const [siteId, setSiteId] = useState(null); - const [siteName, setSiteName] = useState(null); + const [appId, setAppId] = useState(null); + const [appName, setAppName] = useState(null); const [siteLlmsContent, setSiteLlmsContent] = useState(''); const [hostName, setHostName] = useState(''); const [isDefault, setIsDefault] = useState(true) @@ -34,18 +34,18 @@ Optional details go here }; const handleShowEditModal = async () => { - await axios.get(import.meta.env.VITE_APP_SITES_LIST) + await axios.get(import.meta.env.VITE_APP_APPLICATIONS_LIST) .then((response) => { if (response.data && response.data && Array.isArray(response.data)){ - setSiteCollection(response.data); + setAppCollection(response.data); if(response.data.length > 0){ - var firstSite = response.data[0]; - var hosts = firstSite.availableHosts ?? []; - setSiteId(firstSite.siteId); - setSiteName(firstSite.siteName); + var firstApp = response.data[0]; + var hosts = firstApp.availableHosts ?? []; + setAppId(firstApp.appId); + setAppName(firstApp.appName); setHostCollection(hosts); if (hosts.length > 0){ - setHostName(hosts[0].value); + setHostName(hosts[0].hostName); } } @@ -53,27 +53,27 @@ Optional details go here setShowModal(true); } else{ - handleShowFailureToast('Failure', 'Failed to retrieve site data.'); + handleShowFailureToast('Failure', 'Failed to retrieve application data.'); } }, () => { - handleShowFailureToast('Failure', 'Failed to retrieve site data.'); + handleShowFailureToast('Failure', 'Failed to retrieve application data.'); }); } const handleSaveLlmsContent = async () => { - let selectedSite = getSelectedSite(); + let selectedApp = getSelectedApp(); let selectedHost = getSelectedHostName(); let params = new URLSearchParams(); - params.append('siteId', selectedSite.siteId); - params.append('siteName', selectedSite.siteName); + params.append('appId', selectedApp.appId); + params.append('appName', selectedApp.appName); params.append('specificHost', selectedHost); params.append('llmsContent', siteLlmsContent); await axios.post(import.meta.env.VITE_APP_LLMS_SAVE, params) .then(() => { - handleShowSuccessToast('Success', 'Your llms.txt content changes for \'' + siteName + '\' were successfully applied.'); + handleShowSuccessToast('Success', 'Your llms.txt content changes for \'' + appName + '\' were successfully applied.'); setShowModal(false); handleReload(); }, @@ -89,14 +89,14 @@ Optional details go here }); } - const handleSiteSelection = (event) => { - const selectedSiteid = event.target.value; - const selectedSite = siteCollection.filter(x => x.siteId == selectedSiteid)[0]; - const availableHosts = selectedSite.availableHosts ?? []; - const firstHost = availableHosts.length > 0 ? availableHosts[0].value : ''; + const handleAppSelection = (event) => { + const selectedAppId = event.target.value; + const selectedApp = appCollection.filter(x => x.appId == selectedAppId)[0]; + const availableHosts = selectedApp.availableHosts ?? []; + const firstHost = availableHosts.length > 0 ? availableHosts[0].hostName : ''; - setSiteId(selectedSite.siteId); - setSiteName(selectedSite.siteName); + setAppId(selectedApp.appId); + setAppName(selectedApp.appName); setHostName(firstHost); setHostCollection(availableHosts); } @@ -111,11 +111,11 @@ Optional details go here setSiteLlmsContent(event.target.value); } - const renderAvailableSites = () => { - return siteCollection && siteCollection.map((site, index) => { - const { siteId, siteName } = site + const renderAvailableApps = () => { + return appCollection && appCollection.map((app, index) => { + const { appId, appName } = app return ( - + ) }) } @@ -129,22 +129,22 @@ Optional details go here }) } - const getSelectedSite = () => { - if (siteId === undefined || siteId === null || siteId === '') { - var firstSite = siteCollection[0]; - setSiteId(firstSite.siteId); - setSiteName(firstSite.siteName); + const getSelectedApp = () => { + if (appId === undefined || appId === null || appId === '') { + var firstApp = appCollection[0]; + setAppId(firstApp.appId); + setAppName(firstApp.appName); - return firstSite; + return firstApp; } - var matches = siteCollection.filter(matchSite); + var matches = appCollection.filter(matchApp); return matches[0]; } - const matchSite = (thisSite) => { - return thisSite && thisSite.siteId && thisSite.siteId === siteId; + const matchApp = (thisApp) => { + return thisApp && thisApp.appId && thisApp.appId === appId; } const getSelectedHostName = () => { @@ -170,8 +170,8 @@ Optional details go here
- - + +
@@ -183,7 +183,7 @@ Optional details go here
diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteRobots.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteRobots.jsx index e8e449c..e020a6d 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteRobots.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/AddSiteRobots.jsx @@ -5,10 +5,10 @@ import { Button, Modal } from 'react-bootstrap' function AddSiteRobots(props) { const [showModal, setShowModal] = useState(false) - const [siteCollection, setSiteCollection] = useState([]) + const [appCollection, setAppCollection] = useState([]) const [hostCollection, setHostCollection] = useState([]) - const [siteId, setSiteId] = useState(null); - const [siteName, setSiteName] = useState(null); + const [appId, setAppId] = useState(null); + const [appName, setAppName] = useState(null); const [siteRobotsContent, setSiteRobotsContent] = useState('') const [hostName, setHostName] = useState(''); @@ -17,18 +17,18 @@ function AddSiteRobots(props) { } const handleShowEditModal = async () => { - await axios.get(import.meta.env.VITE_APP_SITES_LIST) + await axios.get(import.meta.env.VITE_APP_APPLICATIONS_LIST) .then((response) => { if (response.data && response.data && Array.isArray(response.data)){ - setSiteCollection(response.data); + setAppCollection(response.data); if(response.data.length > 0){ - var firstSite = response.data[0]; - var hosts = firstSite.availableHosts ?? []; - setSiteId(firstSite.siteId); - setSiteName(firstSite.siteName); + var firstApp = response.data[0]; + var hosts = firstApp.availableHosts ?? []; + setAppId(firstApp.appId); + setAppName(firstApp.appName); setHostCollection(hosts); if (hosts.length > 0){ - setHostName(hosts[0].value); + setHostName(hosts[0].hostName); } } @@ -36,27 +36,27 @@ function AddSiteRobots(props) { setShowModal(true); } else{ - handleShowFailureToast('Failure', 'Failed to retrieve site data.'); + handleShowFailureToast('Failure', 'Failed to retrieve application data.'); } }, () => { - handleShowFailureToast('Failure', 'Failed to retrieve site data.'); + handleShowFailureToast('Failure', 'Failed to retrieve application data.'); }); } const handleSaveRobotsContent = async () => { - let selectedSite = getSelectedSite(); + let selectedApp = getSelectedApp(); let selectedHost = getSelectedHostName(); let params = new URLSearchParams(); - params.append('siteId', selectedSite.siteId); - params.append('siteName', selectedSite.siteName); + params.append('appId', selectedApp.appId); + params.append('appName', selectedApp.appName); params.append('specificHost', selectedHost); params.append('robotsContent', siteRobotsContent); await axios.post(import.meta.env.VITE_APP_ROBOTS_SAVE, params) .then(() => { - handleShowSuccessToast('Success', 'Your robots.txt content changes for \'' + siteName + '\' were successfully applied.'); + handleShowSuccessToast('Success', 'Your robots.txt content changes for \'' + appName + '\' were successfully applied.'); setShowModal(false); handleReload(); }, @@ -72,14 +72,14 @@ function AddSiteRobots(props) { }); } - const handleSiteSelection = (event) => { - const selectedSiteid = event.target.value; - const selectedSite = siteCollection.filter(x => x.siteId == selectedSiteid)[0]; - const availableHosts = selectedSite.availableHosts ?? []; - const firstHost = availableHosts.length > 0 ? availableHosts[0].value : ''; + const handleAppSelection = (event) => { + const selectedAppId = event.target.value; + const selectedApp = appCollection.filter(x => x.appId == selectedAppId)[0]; + const availableHosts = selectedApp.availableHosts ?? []; + const firstHost = availableHosts.length > 0 ? availableHosts[0].hostName : ''; - setSiteId(selectedSite.siteId); - setSiteName(selectedSite.siteName); + setAppId(selectedApp.appId); + setAppName(selectedApp.appName); setHostName(firstHost); setHostCollection(availableHosts); } @@ -92,11 +92,11 @@ function AddSiteRobots(props) { setSiteRobotsContent(event.target.value); } - const renderAvailableSites = () => { - return siteCollection && siteCollection.map((site, index) => { - const { siteId, siteName } = site + const renderAvailableApps = () => { + return appCollection && appCollection.map((app, index) => { + const { appId, appName } = app return ( - + ) }) } @@ -110,22 +110,22 @@ function AddSiteRobots(props) { }) } - const getSelectedSite = () => { - if (siteId === undefined || siteId === null || siteId === '') { - var firstSite = siteCollection[0]; - setSiteId(firstSite.siteId); - setSiteName(firstSite.siteName); + const getSelectedApp = () => { + if (appId === undefined || appId === null || appId === '') { + var firstApp = appCollection[0]; + setAppId(firstApp.appId); + setAppName(firstApp.appName); - return firstSite; + return firstApp; } - var matches = siteCollection.filter(matchSite); + var matches = appCollection.filter(matchApp); return matches[0]; } - const matchSite = (thisSite) => { - return thisSite && thisSite.siteId && thisSite.siteId === siteId; + const matchApp = (thisApp) => { + return thisApp && thisApp.appId && thisApp.appId === appId; } const getSelectedHostName = () => { @@ -151,8 +151,8 @@ function AddSiteRobots(props) {
- - + +
diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/ConfigurationList.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/ConfigurationList.jsx index c1c834b..c12605a 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/ConfigurationList.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/ConfigurationList.jsx @@ -8,22 +8,22 @@ import DeleteSiteRobots from './DeleteSiteRobots'; function ConfigurationList(props) { - const [siteCollection, setSiteCollection] = useState([]) + const [appCollection, setAppCollection] = useState([]) useEffect(() => { - getSiteCollection() + getAppCollection() }, []) const handleShowFailureToast = (title, description) => props.showToastNotificationEvent && props.showToastNotificationEvent(false, title, description) - const getSiteCollection = async () => { - - setSiteCollection([]); - + const getAppCollection = async () => { + + setAppCollection([]); + await axios.get(import.meta.env.VITE_APP_ROBOTS_LIST) .then((response) => { if (response.data && response.data.list && Array.isArray(response.data.list)){ - setSiteCollection(response.data.list); + setAppCollection(response.data.list); } else{ handleShowFailureToast('Failure', 'Failed to retrieve robots configuration data.'); @@ -34,17 +34,17 @@ function ConfigurationList(props) }); } - const renderSiteList = () => { - return siteCollection && siteCollection.map((siteDetails, index) => { - const { id, siteId, siteName, isForWholeSite, specificHost, canDelete } = siteDetails + const renderAppList = () => { + return appCollection && appCollection.map((appDetails, index) => { + const { id, appId, appName, isForWholeSite, specificHost, canDelete } = appDetails const hostName = isForWholeSite === true ? 'Default' : specificHost; return ( - {siteName} + {appName} {hostName} - - + + ) @@ -58,20 +58,20 @@ function ConfigurationList(props) A default configuration will always be shown for each site to reflect the fallback behaviour of the AddOn.
- +
- + - {renderSiteList()} + {renderAppList()}
Site NameApplication Host Actions
diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteLlms.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteLlms.jsx index 442ca91..8e8b3fc 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteLlms.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteLlms.jsx @@ -18,12 +18,12 @@ function DeleteSiteLlms(props) { let url = ''.concat(import.meta.env.VITE_APP_LLMS_DELETE, props.id, '/'); await axios.delete(url) .then(() => { - handleShowSuccessToast('Success', 'Your llms content for \'' + props.siteName + '\' was successfully deleted.'); + handleShowSuccessToast('Success', 'Your llms content for \'' + props.appName + '\' was successfully deleted.'); setShowModal(false); handleReload(); }, () => { - handleShowFailureToast('Failure', 'An error was encountered when trying to delete your llms content for \'' + props.siteName + '\'.'); + handleShowFailureToast('Failure', 'An error was encountered when trying to delete your llms content for \'' + props.appName + '\'.'); setShowModal(false); }); } @@ -40,7 +40,7 @@ function DeleteSiteLlms(props) { Delete LLMS Configuration - + diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteRobots.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteRobots.jsx index c92555e..4c7737b 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteRobots.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/DeleteSiteRobots.jsx @@ -18,12 +18,12 @@ function DeleteSiteRobots(props) { let url = ''.concat(import.meta.env.VITE_APP_ROBOTS_DELETE, props.id, '/'); await axios.delete(url) .then(() => { - handleShowSuccessToast('Success', 'Your robots content for \'' + props.siteName + '\' was successfully deleted.'); + handleShowSuccessToast('Success', 'Your robots content for \'' + props.appName + '\' was successfully deleted.'); setShowModal(false); handleReload(); }, () => { - handleShowFailureToast('Failure', 'An error was encountered when trying to delete your robots.txt content for \'' + props.siteName + '\'.'); + handleShowFailureToast('Failure', 'An error was encountered when trying to delete your robots.txt content for \'' + props.appName + '\'.'); setShowModal(false); }); } @@ -40,7 +40,7 @@ function DeleteSiteRobots(props) { Delete Robots Configuration - + diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteLlms.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteLlms.jsx index b37d7c7..bcaad7c 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteLlms.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteLlms.jsx @@ -6,8 +6,8 @@ function EditSiteLlms(props) { const [showModal, setShowModal] = useState(false) const [id, setId] = useState(props.id ?? '') - const [siteId, setSiteId] = useState(props.siteId ?? '') - const [siteName, setSiteName] = useState('') + const [appId, setAppId] = useState(props.appId ?? '') + const [appName, setAppName] = useState('') const [siteLlmsContent, setSiteLlmsContent] = useState('') const [availableHosts, setAvailableHosts] = useState([]) const [isDefault, setIsDefault] = useState(true) @@ -23,12 +23,12 @@ function EditSiteLlms(props) { } const handleShowEditModal = async () => { - await axios.get(import.meta.env.VITE_APP_LLMS_EDIT, { params: { id: id, siteId: siteId } }) + await axios.get(import.meta.env.VITE_APP_LLMS_EDIT, { params: { id: id, appId: appId } }) .then((response) => { if (response.data) { setId(response.data.id); - setSiteId(response.data.siteId); - setSiteName(response.data.siteName); + setAppId(response.data.appId); + setAppName(response.data.appName); setSiteLlmsContent(response.data.llmsContent); setAvailableHosts(response.data.availableHosts ?? []); setIsDefault(response.data.isForWholeSite ?? true); @@ -47,14 +47,14 @@ function EditSiteLlms(props) { const handleSaveLlmsContent = async () => { let params = new URLSearchParams(); params.append('id', id); - params.append('siteId', siteId); - params.append('siteName', siteName); + params.append('appId', appId); + params.append('appName', appName); params.append('specificHost', specificHost); params.append('llmsContent', siteLlmsContent); await axios.post(import.meta.env.VITE_APP_LLMS_SAVE, params) .then(() => { - handleShowSuccessToast('Success', 'Your llms.txt content changes for \'' + siteName + '\' were successfully applied.'); + handleShowSuccessToast('Success', 'Your llms.txt content changes for \'' + appName + '\' were successfully applied.'); setShowModal(false); handleReload(); }, @@ -64,7 +64,7 @@ function EditSiteLlms(props) { setShowModal(false); } else { - handleShowFailureToast('Failure', 'An error was encountered when trying to save your llms.txt content for \'' + siteName + '\'.'); + handleShowFailureToast('Failure', 'An error was encountered when trying to save your llms.txt content for \'' + appName + '\'.'); setShowModal(false); } }); @@ -93,7 +93,7 @@ function EditSiteLlms(props) { - {siteName} + {appName}
diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteRobots.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteRobots.jsx index f58e87f..40f2e7f 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteRobots.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/EditSiteRobots.jsx @@ -6,8 +6,8 @@ function EditSiteRobots(props) { const [showModal, setShowModal] = useState(false) const [id, setId] = useState(props.id ?? '') - const [siteId, setSiteId] = useState(props.siteId ?? '') - const [siteName, setSiteName] = useState('') + const [appId, setAppId] = useState(props.appId ?? '') + const [appName, setAppName] = useState('') const [siteRobotsContent, setSiteRobotsContent] = useState('') const [availableHosts, setAvailableHosts] = useState([]) const [isDefault, setIsDefault] = useState(true) @@ -23,12 +23,12 @@ function EditSiteRobots(props) { } const handleShowEditModal = async () => { - await axios.get(import.meta.env.VITE_APP_ROBOTS_EDIT, { params: { id: id, siteId: siteId } }) + await axios.get(import.meta.env.VITE_APP_ROBOTS_EDIT, { params: { id: id, appId: appId } }) .then((response) => { if (response.data) { setId(response.data.id); - setSiteId(response.data.siteId); - setSiteName(response.data.siteName); + setAppId(response.data.appId); + setAppName(response.data.appName); setSiteRobotsContent(response.data.robotsContent); setAvailableHosts(response.data.availableHosts ?? []); setIsDefault(response.data.isForWholeSite ?? true); @@ -47,14 +47,14 @@ function EditSiteRobots(props) { const handleSaveRobotsContent = async () => { let params = new URLSearchParams(); params.append('id', id); - params.append('siteId', siteId); - params.append('siteName', siteName); + params.append('appId', appId); + params.append('appName', appName); params.append('specificHost', specificHost); params.append('robotsContent', siteRobotsContent); await axios.post(import.meta.env.VITE_APP_ROBOTS_SAVE, params) .then(() => { - handleShowSuccessToast('Success', 'Your robots.txt content changes for \'' + siteName + '\' were successfully applied.'); + handleShowSuccessToast('Success', 'Your robots.txt content changes for \'' + appName + '\' were successfully applied.'); setShowModal(false); handleReload(); }, @@ -64,7 +64,7 @@ function EditSiteRobots(props) { setShowModal(false); } else { - handleShowFailureToast('Failure', 'An error was encountered when trying to save your robots.txt content for \'' + siteName + '\'.'); + handleShowFailureToast('Failure', 'An error was encountered when trying to save your robots.txt content for \'' + appName + '\'.'); setShowModal(false); } }); @@ -93,7 +93,7 @@ function EditSiteRobots(props) { - {siteName} + {appName}
@@ -117,4 +117,4 @@ function EditSiteRobots(props) { ) } -export default EditSiteRobots +export default EditSiteRobots \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/LlmsConfigurationList.jsx b/src/Stott.Optimizely.RobotsHandler.Ui/src/LlmsConfigurationList.jsx index f5612d4..6982862 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/LlmsConfigurationList.jsx +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/LlmsConfigurationList.jsx @@ -8,22 +8,22 @@ import AddSiteLlms from './AddSiteLlms'; function LlmsConfigurationList(props) { - const [siteCollection, setSiteCollection] = useState([]) + const [appCollection, setAppCollection] = useState([]) useEffect(() => { - getSiteCollection() + getAppCollection() }, []) const handleShowFailureToast = (title, description) => props.showToastNotificationEvent && props.showToastNotificationEvent(false, title, description) - const getSiteCollection = async () => { - - setSiteCollection([]); + const getAppCollection = async () => { + + setAppCollection([]); await axios.get(import.meta.env.VITE_APP_LLMS_LIST) .then((response) => { if (response.data && response.data.list && Array.isArray(response.data.list)){ - setSiteCollection(response.data.list); + setAppCollection(response.data.list); } else{ handleShowFailureToast('Failure', 'Failed to retrieve llms configuration data.'); @@ -34,17 +34,17 @@ function LlmsConfigurationList(props) }); } - const renderSiteList = () => { - return siteCollection && siteCollection.map((siteDetails, index) => { - const { id, siteId, siteName, isForWholeSite, specificHost } = siteDetails + const renderAppList = () => { + return appCollection && appCollection.map((appDetails, index) => { + const { id, appId, appName, isForWholeSite, specificHost } = appDetails const hostName = isForWholeSite === true ? 'Default' : specificHost; return ( - {siteName} + {appName} {hostName} - - + + ) @@ -58,20 +58,20 @@ function LlmsConfigurationList(props) An llms.txt file should be written in markdown as it is human-readable and helps AI (like Large Language Models) understand a website's content and purpose. You can learn more here.
- +
- + - {renderSiteList()} + {renderAppList()}
Site NameApplication Host Actions
diff --git a/src/Stott.Optimizely.RobotsHandler.Ui/src/apiEndpointsData.js b/src/Stott.Optimizely.RobotsHandler.Ui/src/apiEndpointsData.js index 4a40d7e..d5d9d00 100644 --- a/src/Stott.Optimizely.RobotsHandler.Ui/src/apiEndpointsData.js +++ b/src/Stott.Optimizely.RobotsHandler.Ui/src/apiEndpointsData.js @@ -42,7 +42,7 @@ Authorization: bearer token-value`, responseJson: `[ { "id": "guid", - "siteName": "string", + "appName": "string", "isDefaultForSite": boolean, "specificHost": "string", "robotsContent": "string" @@ -66,8 +66,8 @@ Authorization: bearer token-value`, "message": "string", "data": { "id": "guid", - "siteId": "guid", - "siteName": "string", + "appId": "string", + "appName": "string", "specificHost": "string", "robotsContent": "string" } @@ -86,7 +86,7 @@ Authorization: bearer token-value`, responseJson: `[ { "id": "guid", - "siteName": "string", + "appName": "string", "isDefaultForSite": boolean, "specificHost": "string", "llmsContent": "string" @@ -110,11 +110,11 @@ Authorization: bearer token-value`, "message": "string", "data": { "id": "guid", - "siteId": "guid", - "siteName": "string", + "appId": "string", + "appName": "string", "specificHost": "string", "llmsContent": "string" } }` } -}; +}; \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionController.cs b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionController.cs new file mode 100644 index 0000000..f5daf07 --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionController.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +using Stott.Optimizely.RobotsHandler.Common; + +namespace Stott.Optimizely.RobotsHandler.Applications; + +[ApiExplorerSettings(IgnoreApi = true)] +[Authorize(Policy = RobotsConstants.AuthorizationPolicy)] +public sealed class ApplicationDefinitionController(IApplicationDefinitionService appService) : BaseApiController +{ + [HttpGet] + [Route("/stott.robotshandler/api/[action]")] + public async Task Applications() + { + var apps = await appService.GetAllApplicationsAsync(); + var allApps = apps.ToList(); + + allApps.Insert(0, new ApplicationViewModel + { + AppName = "All Applications", + AvailableHosts = ApplicationMapper.CreateHostSummaries("All Hosts") + }); + + return CreateSafeJsonResult(allApps); + } +} diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionService.cs b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionService.cs new file mode 100644 index 0000000..291c565 --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationDefinitionService.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using EPiServer.Applications; + +namespace Stott.Optimizely.RobotsHandler.Applications; + +public sealed class ApplicationDefinitionService(IApplicationRepository applicationRepository) : IApplicationDefinitionService +{ + public async Task> GetAllApplicationsAsync() + { + var data = await applicationRepository.ListAsync(); + return ToModels(data); + } + + public async Task GetApplicationByIdAsync(string? appId) + { + if (string.IsNullOrWhiteSpace(appId)) + { + return null; + } + + var application = await applicationRepository.GetAsync(appId); + + return application switch + { + Website website => ToModel(website), + InProcessWebsite inprocessWebsite => ToModel(inprocessWebsite), + _ => null + }; + } + + private static IEnumerable ToModels(IEnumerable applications) + { + if (!applications.Any()) + { + yield break; + } + + foreach (var application in applications) + { + if (application is Website website) + { + yield return ToModel(website); + } + else if (application is InProcessWebsite inprocessWebsite) + { + yield return ToModel(inprocessWebsite); + } + } + } + + private static ApplicationViewModel ToModel(Website website) + { + return new ApplicationViewModel + { + AppId = website.Name, + AppName = website.DisplayName, + AvailableHosts = [.. ApplicationMapper.CreateHostSummaries(website.Hosts)] + }; + } + + private static ApplicationViewModel ToModel(InProcessWebsite website) + { + return new ApplicationViewModel + { + AppId = website.Name, + AppName = website.DisplayName, + AvailableHosts = [.. ApplicationMapper.CreateHostSummaries(website.Hosts)] + }; + } +} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationMapper.cs b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationMapper.cs new file mode 100644 index 0000000..be69f0d --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationMapper.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; + +using EPiServer.Applications; + +using Stott.Optimizely.RobotsHandler.Extensions; + +namespace Stott.Optimizely.RobotsHandler.Applications; + +internal static class ApplicationMapper +{ + internal static List CreateHostSummaries(string defaultHostName) + { + return + [ + new HostViewModel + { + DisplayName = defaultHostName, + HostName = string.Empty + } + ]; + } + + internal static IEnumerable CreateHostSummaries(IList? hostDefinitions) + { + if (hostDefinitions is not { Count: > 0 }) + { + yield break; + } + + yield return new HostViewModel { DisplayName = "Default", HostName = string.Empty }; + + if (hostDefinitions is not { Count: > 0 }) + { + yield break; + } + + foreach (var host in hostDefinitions.Where(x => x.Url is not null)) + { + yield return new HostViewModel { DisplayName = host.Url?.ToString(), HostName = host.Url.GetSanitizedHostDomain() }; + } + } +} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationViewModel.cs new file mode 100644 index 0000000..82765f7 --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/ApplicationViewModel.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Stott.Optimizely.RobotsHandler.Applications; + +public sealed class ApplicationViewModel +{ + public string? AppId { get; set; } + + public string? AppName { get; set; } + + public List AvailableHosts { get; set; } = []; +} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/HostViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Applications/HostViewModel.cs new file mode 100644 index 0000000..2f3e4bd --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/HostViewModel.cs @@ -0,0 +1,8 @@ +namespace Stott.Optimizely.RobotsHandler.Applications; + +public sealed class HostViewModel +{ + public string? DisplayName { get; set; } + + public string? HostName { get; set; } +} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Applications/IApplicationDefinitionService.cs b/src/Stott.Optimizely.RobotsHandler/Applications/IApplicationDefinitionService.cs new file mode 100644 index 0000000..5cf56a1 --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Applications/IApplicationDefinitionService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Stott.Optimizely.RobotsHandler.Applications; + +public interface IApplicationDefinitionService +{ + Task> GetAllApplicationsAsync(); + + Task GetApplicationByIdAsync(string? appId); +} diff --git a/src/Stott.Optimizely.RobotsHandler/Cache/IRobotsCacheHandler.cs b/src/Stott.Optimizely.RobotsHandler/Cache/IRobotsCacheHandler.cs index 7da11dc..9b7bf33 100644 --- a/src/Stott.Optimizely.RobotsHandler/Cache/IRobotsCacheHandler.cs +++ b/src/Stott.Optimizely.RobotsHandler/Cache/IRobotsCacheHandler.cs @@ -4,7 +4,7 @@ public interface IRobotsCacheHandler { void Add(string cacheKey, T objectToCache) where T : class; - T Get(string cacheKey) where T : class; + T? Get(string cacheKey) where T : class; void RemoveAll(); } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Cache/RobotsCacheHandler.cs b/src/Stott.Optimizely.RobotsHandler/Cache/RobotsCacheHandler.cs index 7777fcb..51cae8d 100644 --- a/src/Stott.Optimizely.RobotsHandler/Cache/RobotsCacheHandler.cs +++ b/src/Stott.Optimizely.RobotsHandler/Cache/RobotsCacheHandler.cs @@ -7,20 +7,12 @@ namespace Stott.Optimizely.RobotsHandler.Cache; -public sealed class RobotsCacheHandler : IRobotsCacheHandler +public sealed class RobotsCacheHandler( + ISynchronizedObjectInstanceCache cache, + ILogger logger) : IRobotsCacheHandler { - private readonly ISynchronizedObjectInstanceCache _cache; - - private readonly ILogger _logger; - private const string MasterKey = "Stott-RobotsHandler-MasterKey"; - public RobotsCacheHandler(ISynchronizedObjectInstanceCache cache, ILogger logger) - { - _cache = cache; - _logger = logger; - } - public void Add(string cacheKey, T objectToCache) where T : class { @@ -31,21 +23,17 @@ public void Add(string cacheKey, T objectToCache) try { - var evictionPolicy = new CacheEvictionPolicy( - TimeSpan.FromHours(12), - CacheTimeoutType.Absolute, - Enumerable.Empty(), - new[] { MasterKey }); + var evictionPolicy = new CacheEvictionPolicy(TimeSpan.FromHours(12), CacheTimeoutType.Absolute, [], [MasterKey]); - _cache.Insert(cacheKey, objectToCache, evictionPolicy); + cache.Insert(cacheKey, objectToCache, evictionPolicy); } catch (Exception exception) { - _logger.LogError(exception, "[Robots Handler] Failed to add item to cache with a key of {cacheKey}.", cacheKey); + logger.LogError(exception, "[Robots Handler] Failed to add item to cache with a key of {cacheKey}.", cacheKey); } } - public T Get(string cacheKey) + public T? Get(string cacheKey) where T : class { if (string.IsNullOrWhiteSpace(cacheKey)) @@ -53,18 +41,18 @@ public T Get(string cacheKey) return null; } - return _cache.TryGet(cacheKey, ReadStrategy.Wait, out var cachedObject) ? cachedObject : default; + return cache.TryGet(cacheKey, ReadStrategy.Wait, out var cachedObject) ? cachedObject : default; } public void RemoveAll() { try { - _cache.Remove(MasterKey); + cache.Remove(MasterKey); } catch (Exception exception) { - _logger.LogError(exception, "[Robots Handler] Failed to remove all items from cache based on the master key."); + logger.LogError(exception, "[Robots Handler] Failed to remove all items from cache based on the master key."); } } } diff --git a/src/Stott.Optimizely.RobotsHandler/Common/ISiteContentModel.cs b/src/Stott.Optimizely.RobotsHandler/Common/ISiteContentModel.cs index a4ae47e..5b9c1ee 100644 --- a/src/Stott.Optimizely.RobotsHandler/Common/ISiteContentModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Common/ISiteContentModel.cs @@ -1,21 +1,21 @@ using System; using System.Collections.Generic; -using Stott.Optimizely.RobotsHandler.Sites; +using Stott.Optimizely.RobotsHandler.Applications; namespace Stott.Optimizely.RobotsHandler.Common; -public interface ISiteContentViewModel +public interface IApplicationContentViewModel { public Guid Id { get; set; } - public Guid SiteId { get; set; } + public string? AppId { get; set; } - public string SiteName { get; set; } + public string? AppName { get; set; } - public List AvailableHosts { get; set; } + public List AvailableHosts { get; set; } public bool IsForWholeSite { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } } diff --git a/src/Stott.Optimizely.RobotsHandler/Configuration/RobotsServiceExtensions.cs b/src/Stott.Optimizely.RobotsHandler/Configuration/RobotsServiceExtensions.cs index e53779c..3ab7d3f 100644 --- a/src/Stott.Optimizely.RobotsHandler/Configuration/RobotsServiceExtensions.cs +++ b/src/Stott.Optimizely.RobotsHandler/Configuration/RobotsServiceExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Cache; using Stott.Optimizely.RobotsHandler.Common; using Stott.Optimizely.RobotsHandler.Environments; @@ -25,7 +25,7 @@ public static class ServiceExtensions /// public static IServiceCollection AddRobotsHandler( this IServiceCollection serviceCollection, - Action authorizationOptions = null) + Action? authorizationOptions = null) { serviceCollection.AddScoped(); serviceCollection.AddScoped(); @@ -38,6 +38,7 @@ public static IServiceCollection AddRobotsHandler( serviceCollection.AddScoped(); serviceCollection.AddScoped(provider => new Lazy(() => provider.GetRequiredService())); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); // Authorization if (authorizationOptions != null) diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentApiController.cs b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentApiController.cs index 46d6cad..6f1732c 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentApiController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentApiController.cs @@ -11,25 +11,15 @@ namespace Stott.Optimizely.RobotsHandler.Environments; [ApiExplorerSettings(IgnoreApi = true)] [Authorize(Policy = RobotsConstants.AuthorizationPolicy)] -public sealed class EnvironmentApiController : BaseApiController +public sealed class EnvironmentApiController( + IEnvironmentRobotsService service, + ILogger logger) : BaseApiController { - private readonly IEnvironmentRobotsService _service; - - private readonly ILogger _logger; - - public EnvironmentApiController( - IEnvironmentRobotsService service, - ILogger logger) - { - _service = service; - _logger = logger; - } - [HttpGet] [Route("/stott.robotshandler/api/environment/[action]")] public IActionResult List() { - var model = _service.GetAll(); + var model = service.GetAll(); return CreateSafeJsonResult(model); } @@ -40,13 +30,13 @@ public IActionResult Save(EnvironmentRobotsModel model) { try { - _service.Save(model); + service.Save(model); return new OkResult(); } catch (Exception exception) { - _logger.LogError(exception, "Failed to save robots.txt content for {environment}", model.EnvironmentName); + logger.LogError(exception, "Failed to save robots.txt content for {environment}", model.EnvironmentName); return new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsModel.cs b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsModel.cs index d438e7e..3559359 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsModel.cs @@ -7,7 +7,7 @@ public sealed class EnvironmentRobotsModel { public Guid Id { get; set; } - public string EnvironmentName { get; set; } + public string? EnvironmentName { get; set; } public bool UseNoFollow { get; set; } diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsRepository.cs b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsRepository.cs index 7027801..5f5c3b8 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsRepository.cs @@ -17,8 +17,13 @@ public EnvironmentRobotsRepository() store = DynamicDataStoreFactory.Instance.CreateStore(typeof(EnvironmentRobotsEntity)); } - public EnvironmentRobotsModel Get(string environmentName) + public EnvironmentRobotsModel? Get(string? environmentName) { + if (string.IsNullOrWhiteSpace(environmentName)) + { + return null; + } + return store.Find(new Dictionary { { nameof(EnvironmentRobotsEntity.EnvironmentName), environmentName } }) .Select(x => ToModel(x)) .FirstOrDefault(); @@ -26,13 +31,16 @@ public EnvironmentRobotsModel Get(string environmentName) public IList GetAll() { - return store.Find(new Dictionary()) - .Select(x => ToModel(x)) - .ToList(); + return [.. store.Find(new Dictionary()).Select(ToModel)]; } public void Save(EnvironmentRobotsModel model) { + if (string.IsNullOrWhiteSpace(model.EnvironmentName)) + { + throw new ArgumentException($"'{nameof(model.EnvironmentName)}' cannot be null or whitespace.", nameof(model)); + } + var recordToSave = store.Find(new Dictionary { { nameof(EnvironmentRobotsEntity.EnvironmentName), model.EnvironmentName } }).FirstOrDefault(); recordToSave ??= new EnvironmentRobotsEntity { diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsService.cs b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsService.cs index 4aa1cd1..75cbf11 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/EnvironmentRobotsService.cs @@ -9,40 +9,24 @@ namespace Stott.Optimizely.RobotsHandler.Environments; -public sealed class EnvironmentRobotsService : IEnvironmentRobotsService +public sealed class EnvironmentRobotsService( + Lazy repository, + IWebHostEnvironment hostingEnvironment, + IRobotsCacheHandler cacheHandler) : IEnvironmentRobotsService { - private readonly Lazy _repository; - - private readonly IWebHostEnvironment _hostingEnvironment; - - private readonly IRobotsCacheHandler _cacheHandler; - - public EnvironmentRobotsService( - Lazy repository, - IWebHostEnvironment hostingEnvironment, - IRobotsCacheHandler cacheHandler) - { - _repository = repository; - _hostingEnvironment = hostingEnvironment; - _cacheHandler = cacheHandler; - } - public IList GetAll() { - var currentEnvironment = _hostingEnvironment.EnvironmentName; - var configurations = _repository.Value.GetAll() ?? new List(0); + var currentEnvironment = hostingEnvironment.EnvironmentName; + var configurations = repository.Value.GetAll() ?? []; IncludeAllEnvironments(configurations, currentEnvironment); var currentConfig = configurations.FirstOrDefault(x => string.Equals(x.EnvironmentName, currentEnvironment, StringComparison.OrdinalIgnoreCase)); - if (currentConfig != null) - { - currentConfig.IsCurrentEnvironment = true; - } + currentConfig?.IsCurrentEnvironment = true; - return configurations.OrderByDescending(x => x.IsCurrentEnvironment).ThenBy(x => x.EnvironmentName).ToList(); + return [.. configurations.OrderByDescending(x => x.IsCurrentEnvironment).ThenBy(x => x.EnvironmentName)]; } - public EnvironmentRobotsModel Get(string environmentName) + public EnvironmentRobotsModel? Get(string? environmentName) { if (string.IsNullOrWhiteSpace(environmentName)) { @@ -50,24 +34,24 @@ public EnvironmentRobotsModel Get(string environmentName) } var cacheKey = GetCacheKey(environmentName); - var environmentModel = _cacheHandler.Get(cacheKey); + var environmentModel = cacheHandler.Get(cacheKey); if (environmentModel is not null) { return environmentModel; } - environmentModel = _repository.Value.Get(environmentName); + environmentModel = repository.Value.Get(environmentName); if (environmentModel is not null) { - _cacheHandler.Add(cacheKey, environmentModel); + cacheHandler.Add(cacheKey, environmentModel); } return environmentModel; } - public EnvironmentRobotsModel GetCurrent() + public EnvironmentRobotsModel? GetCurrent() { - return Get(_hostingEnvironment.EnvironmentName); + return Get(hostingEnvironment.EnvironmentName); } public void Save(EnvironmentRobotsModel model) @@ -77,8 +61,8 @@ public void Save(EnvironmentRobotsModel model) return; } - _cacheHandler.RemoveAll(); - _repository.Value.Save(model); + cacheHandler.RemoveAll(); + repository.Value.Save(model); } private static void IncludeAllEnvironments(IList environmentModels, string currentEnvironmentName) diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsRepository.cs b/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsRepository.cs index 52cc5c9..bc1682e 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsRepository.cs @@ -6,7 +6,7 @@ public interface IEnvironmentRobotsRepository { IList GetAll(); - EnvironmentRobotsModel Get(string environmentName); + EnvironmentRobotsModel? Get(string? environmentName); void Save(EnvironmentRobotsModel model); } diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsService.cs b/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsService.cs index 5ad5709..1e21f86 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/IEnvironmentRobotsService.cs @@ -6,9 +6,9 @@ public interface IEnvironmentRobotsService { IList GetAll(); - EnvironmentRobotsModel Get(string environmentName); + EnvironmentRobotsModel? Get(string? environmentName); - EnvironmentRobotsModel GetCurrent(); + EnvironmentRobotsModel? GetCurrent(); void Save(EnvironmentRobotsModel model); } diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/MetaRobotsTagHelper.cs b/src/Stott.Optimizely.RobotsHandler/Environments/MetaRobotsTagHelper.cs index 7a16c6f..cd328bd 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/MetaRobotsTagHelper.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/MetaRobotsTagHelper.cs @@ -5,23 +5,16 @@ namespace Stott.Optimizely.RobotsHandler.Environments; [HtmlTargetElement("meta", Attributes = "name")] -public sealed class MetaRobotsTagHelper : TagHelper +public sealed class MetaRobotsTagHelper(IEnvironmentRobotsService environmentRobotsService) : TagHelper { - private readonly IEnvironmentRobotsService _environmentRobotsService; - public override int Order => -9999; - public MetaRobotsTagHelper(IEnvironmentRobotsService environmentRobotsService) - { - _environmentRobotsService = environmentRobotsService; - } - public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var metaName = GetMetaName(context); if (string.Equals(metaName, "robots", System.StringComparison.OrdinalIgnoreCase)) { - var environmentContent = _environmentRobotsService.GetCurrent(); + var environmentContent = environmentRobotsService.GetCurrent(); var existingContent = GetMetaContent(context); if (environmentContent is { IsEnabled: true }) @@ -37,11 +30,11 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp return base.ProcessAsync(context, output); } - private static string GetMetaName(TagHelperContext context) => GetAttributeValue(context, "name"); + private static string? GetMetaName(TagHelperContext context) => GetAttributeValue(context, "name"); - private static string GetMetaContent(TagHelperContext context) => GetAttributeValue(context, "content"); + private static string? GetMetaContent(TagHelperContext context) => GetAttributeValue(context, "content"); - private static string GetAttributeValue(TagHelperContext context, string attributeName) + private static string? GetAttributeValue(TagHelperContext context, string attributeName) { if (context.AllAttributes.TryGetAttribute(attributeName, out var attribute)) { diff --git a/src/Stott.Optimizely.RobotsHandler/Environments/RobotsHeaderMiddleware.cs b/src/Stott.Optimizely.RobotsHandler/Environments/RobotsHeaderMiddleware.cs index 3df614d..7d4bf38 100644 --- a/src/Stott.Optimizely.RobotsHandler/Environments/RobotsHeaderMiddleware.cs +++ b/src/Stott.Optimizely.RobotsHandler/Environments/RobotsHeaderMiddleware.cs @@ -8,14 +8,9 @@ namespace Stott.Optimizely.RobotsHandler.Environments; -public sealed class RobotsHeaderMiddleware +public sealed class RobotsHeaderMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - - public RobotsHeaderMiddleware(RequestDelegate next) - { - _next = next; - } + private readonly RequestDelegate _next = next; public async Task Invoke( HttpContext context, diff --git a/src/Stott.Optimizely.RobotsHandler/Extensions/SiteDefinitionExtensions.cs b/src/Stott.Optimizely.RobotsHandler/Extensions/SiteDefinitionExtensions.cs deleted file mode 100644 index 7a3d920..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Extensions/SiteDefinitionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using EPiServer.Web; - -using Stott.Optimizely.RobotsHandler.Sites; - -namespace Stott.Optimizely.RobotsHandler.Extensions; - -public static class SiteDefinitionExtensions -{ - public static IEnumerable ToHostSummaries(this IList hostDefinitions) - { - yield return new SiteHostViewModel { DisplayName = "Default", HostName = string.Empty }; - if (hostDefinitions is not { Count: > 0 }) - { - yield break; - } - - foreach (var host in hostDefinitions.Where(x => x.Url is not null)) - { - yield return new SiteHostViewModel { DisplayName = host.Name, HostName = host.Name }; - } - } -} diff --git a/src/Stott.Optimizely.RobotsHandler/Extensions/StringExtensions.cs b/src/Stott.Optimizely.RobotsHandler/Extensions/StringExtensions.cs new file mode 100644 index 0000000..ddf27db --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Extensions/StringExtensions.cs @@ -0,0 +1,23 @@ +namespace Stott.Optimizely.RobotsHandler.Extensions; + +using System; + +internal static class StringExtensions +{ + internal static string? GetSanitizedHostDomain(this string? hostName) + { + if (string.IsNullOrWhiteSpace(hostName)) + { + return null; + } + + var sanitized = hostName.Trim().TrimEnd('/'); + var normalized = sanitized.Contains("://") ? sanitized : $"https://{sanitized}"; + if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri)) + { + return uri.Host + (uri.IsDefaultPort ? string.Empty : ":" + uri.Port); + } + + return sanitized; + } +} diff --git a/src/Stott.Optimizely.RobotsHandler/Extensions/UriExtensions.cs b/src/Stott.Optimizely.RobotsHandler/Extensions/UriExtensions.cs new file mode 100644 index 0000000..950d394 --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Extensions/UriExtensions.cs @@ -0,0 +1,21 @@ +using System; + +namespace Stott.Optimizely.RobotsHandler.Extensions; + +internal static class UriExtensions +{ + internal static string? GetSanitizedHostDomain(this Uri? uri) + { + if (uri is null) + { + return null; + } + + if (!uri.IsAbsoluteUri) + { + return null; + } + + return uri.Host + (uri.IsDefaultPort ? string.Empty : ":" + uri.Port); + } +} diff --git a/src/Stott.Optimizely.RobotsHandler/GlobalSuppressions.cs b/src/Stott.Optimizely.RobotsHandler/GlobalSuppressions.cs new file mode 100644 index 0000000..85d158d --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0270:Use coalesce expression", Justification = "Because it makes code less readable")] diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/ApplicationLlmsViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Llms/ApplicationLlmsViewModel.cs new file mode 100644 index 0000000..db5a05f --- /dev/null +++ b/src/Stott.Optimizely.RobotsHandler/Llms/ApplicationLlmsViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +using Stott.Optimizely.RobotsHandler.Applications; +using Stott.Optimizely.RobotsHandler.Common; + +namespace Stott.Optimizely.RobotsHandler.Llms +{ + public sealed class ApplicationLlmsViewModel : IApplicationContentViewModel + { + public Guid Id { get; set; } + + public string? AppId { get; set; } + + public string? AppName { get; set; } + + public List AvailableHosts { get; set; } = []; + + public bool IsForWholeSite { get; set; } + + public string? SpecificHost { get; set; } + + public string? LlmsContent { get; set; } + } +} diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentRepository.cs b/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentRepository.cs index 64b494c..fe47423 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentRepository.cs @@ -5,6 +5,7 @@ using EPiServer.Data; using EPiServer.Data.Dynamic; +using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Models; namespace Stott.Optimizely.RobotsHandler.Llms; @@ -23,7 +24,7 @@ public void Delete(Guid id) store.Delete(Identity.NewIdentity(id)); } - public LlmsTxtEntity Get(Guid id) + public LlmsTxtEntity? Get(Guid id) { if (Guid.Empty.Equals(id)) { @@ -35,24 +36,26 @@ public LlmsTxtEntity Get(Guid id) public List GetAll() { - return store.Find(new Dictionary()).ToList(); + return [.. store.Find(new Dictionary())]; } - public List GetAllForSite(Guid siteId) + public List GetAllForSite(string? appId) { - return store.Find(new Dictionary { { nameof(LlmsTxtEntity.SiteId), siteId } }).ToList(); + if (string.IsNullOrWhiteSpace(appId)) + { + return []; + } + + return [.. store.Find(new Dictionary { { nameof(LlmsTxtEntity.AppId), appId } })]; } public void Save(SaveLlmsModel model) { var recordToSave = Get(model.Id); - recordToSave ??= new LlmsTxtEntity - { - Id = Identity.NewIdentity(Guid.NewGuid()), - SiteId = model.SiteId, - }; + recordToSave ??= new LlmsTxtEntity { Id = Identity.NewIdentity(Guid.NewGuid()) }; - recordToSave.SpecificHost = model.SpecificHost; + recordToSave.AppId = model.AppId; + recordToSave.SpecificHost = model.SpecificHost.GetSanitizedHostDomain(); recordToSave.IsForWholeSite = string.IsNullOrWhiteSpace(model.SpecificHost); recordToSave.LlmsContent = model.LlmsContent; diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentService.cs b/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentService.cs index 663055f..6ce39ff 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/DefaultLlmsContentService.cs @@ -2,27 +2,16 @@ using System.Collections.Generic; using System.Linq; -using EPiServer.Web; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Models; namespace Stott.Optimizely.RobotsHandler.Llms; -public class DefaultLlmsContentService : ILlmsContentService +public class DefaultLlmsContentService( + IApplicationDefinitionService appService, + ILlmsContentRepository llmsContentRepository) : ILlmsContentService { - private readonly ISiteDefinitionRepository siteDefinitionRepository; - - private readonly ILlmsContentRepository llmsContentRepository; - - public DefaultLlmsContentService( - ISiteDefinitionRepository siteDefinitionRepository, - ILlmsContentRepository llmsContentRepository) - { - this.siteDefinitionRepository = siteDefinitionRepository; - this.llmsContentRepository = llmsContentRepository; - } - public void Delete(Guid id) { if (Guid.Empty.Equals(id)) @@ -35,11 +24,11 @@ public void Delete(Guid id) public bool DoesConflictExists(SaveLlmsModel model) { - var existingConfigurations = llmsContentRepository.GetAll() ?? new List(0); + var existingConfigurations = llmsContentRepository.GetAll() ?? []; return existingConfigurations.Any(x => IsConflict(model, x)); } - public SiteLlmsViewModel Get(Guid id) + public ApplicationLlmsViewModel Get(Guid id) { var robotRecord = llmsContentRepository.Get(id); if (robotRecord == null) @@ -47,54 +36,55 @@ public SiteLlmsViewModel Get(Guid id) throw new RobotsEntityNotFoundException(id); } - var sites = siteDefinitionRepository.List(); - var site = sites.FirstOrDefault(x => x.Id.Equals(robotRecord.SiteId)); - if (site == null) + var applications = appService.GetAllApplicationsAsync().GetAwaiter().GetResult(); + var application = applications.FirstOrDefault(x => string.Equals(x.AppId, robotRecord.AppId, StringComparison.OrdinalIgnoreCase)); + if (application == null) { - throw new RobotsEntityNotFoundException($"Llms entity with id '{id}' not match any site definitions."); + throw new RobotsEntityNotFoundException($"Llms entity with id '{id}' not match any application definitions."); } - return ToModel(robotRecord, site); + return ToModel(robotRecord, application); } - public IList GetAll() + public IList GetAll() { var allRecords = llmsContentRepository.GetAll(); - var sites = siteDefinitionRepository.List(); - var models = new List(); + var applications = appService.GetAllApplicationsAsync().GetAwaiter().GetResult(); + var models = new List(); foreach (var robotRecord in allRecords) { - var site = sites.FirstOrDefault(x => x.Id.Equals(robotRecord.SiteId)); - if (site != null) + var application = applications.FirstOrDefault(x => string.Equals(x.AppId, robotRecord.AppId)); + if (application != null) { - models.Add(ToModel(robotRecord, site)); + models.Add(ToModel(robotRecord, application)); } } - return models.OrderBy(x => x.SiteName).ThenBy(x => x.SpecificHost).ToList(); + return [.. models.OrderBy(x => x.AppName).ThenBy(x => x.SpecificHost)]; } - public SiteLlmsViewModel GetDefault(Guid siteId) + public ApplicationLlmsViewModel GetDefault(string? appId) { - var site = siteDefinitionRepository.Get(siteId); - if (site == null) + var application = appService.GetApplicationByIdAsync(appId).GetAwaiter().GetResult(); + if (application == null) { - throw new ArgumentException($"{nameof(siteId)} does not correlate to a known site.", nameof(siteId)); + throw new ArgumentException($"{nameof(appId)} does not correlate to a known application.", nameof(appId)); } - return ToModel(site); + return ToModel(application); } - public string GetDefaultLlmsContent() + public string? GetDefaultLlmsContent() { return string.Empty; } - public string GetLlmsContent(Guid siteId, string host) + public string? GetLlmsContent(string? appId, string? host) { - var llmsEntries = llmsContentRepository.GetAllForSite(siteId) ?? new List(0); - var matchingLlms = llmsEntries.FirstOrDefault(x => string.Equals(x.SpecificHost, host, StringComparison.OrdinalIgnoreCase)) ?? + var cleanedHost = host.GetSanitizedHostDomain(); + var llmsEntries = llmsContentRepository.GetAllForSite(appId) ?? []; + var matchingLlms = llmsEntries.FirstOrDefault(x => string.Equals(x.SpecificHost, cleanedHost, StringComparison.OrdinalIgnoreCase)) ?? llmsEntries.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.SpecificHost)); return matchingLlms?.LlmsContent; @@ -102,44 +92,44 @@ public string GetLlmsContent(Guid siteId, string host) public void Save(SaveLlmsModel model) { - if (Guid.Empty.Equals(model.SiteId)) + if (Guid.Empty.Equals(model.AppId)) { - throw new ArgumentException($"{nameof(model)}.{nameof(model.SiteId)} must not be null or empty.", nameof(model)); + throw new ArgumentException($"{nameof(model)}.{nameof(model.AppId)} must not be null or empty.", nameof(model)); } - var existingSite = siteDefinitionRepository.Get(model.SiteId); - if (existingSite == null) + var application = appService.GetApplicationByIdAsync(model.AppId).GetAwaiter().GetResult(); + if (application == null) { - throw new ArgumentException($"{nameof(model)}.{nameof(model.SiteId)} does not correlate to a known site.", nameof(model)); + throw new ArgumentException($"{nameof(model)}.{nameof(model.AppId)} does not correlate to a known application.", nameof(model)); } llmsContentRepository.Save(model); } - private static SiteLlmsViewModel ToModel(LlmsTxtEntity entity, SiteDefinition siteDefinition) + private static ApplicationLlmsViewModel ToModel(LlmsTxtEntity entity, ApplicationViewModel application) { - return new SiteLlmsViewModel + return new ApplicationLlmsViewModel { Id = entity.Id.ExternalId, - SiteId = entity.SiteId, + AppId = entity.AppId, IsForWholeSite = entity.IsForWholeSite || string.IsNullOrWhiteSpace(entity.SpecificHost), SpecificHost = entity.SpecificHost, LlmsContent = entity.LlmsContent, - SiteName = siteDefinition.Name, - AvailableHosts = siteDefinition.Hosts.ToHostSummaries().ToList() + AppName = application.AppName, + AvailableHosts = application.AvailableHosts }; } - private SiteLlmsViewModel ToModel(SiteDefinition siteDefinition) + private ApplicationLlmsViewModel ToModel(ApplicationViewModel application) { - return new SiteLlmsViewModel + return new ApplicationLlmsViewModel { Id = Guid.Empty, - SiteId = siteDefinition.Id, + AppId = application.AppId, IsForWholeSite = true, LlmsContent = GetDefaultLlmsContent(), - SiteName = siteDefinition.Name, - AvailableHosts = siteDefinition.Hosts.ToHostSummaries().ToList() + AppName = application.AppName, + AvailableHosts = application.AvailableHosts }; } @@ -148,7 +138,8 @@ private static bool IsConflict(SaveLlmsModel model, LlmsTxtEntity entity) var modelHost = model.SpecificHost ?? string.Empty; var entityHost = entity.SpecificHost ?? string.Empty; - return Equals(model.SiteId, entity.SiteId) && !Equals(model.Id, entity.Id.ExternalId) && + return string.Equals(model.AppId, entity.AppId, StringComparison.OrdinalIgnoreCase) && + !Guid.Equals(model.Id, entity.Id.ExternalId) && string.Equals(modelHost, entityHost, StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentRepository.cs b/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentRepository.cs index 91fb599..71418be 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentRepository.cs @@ -9,9 +9,9 @@ public interface ILlmsContentRepository { List GetAll(); - List GetAllForSite(Guid siteId); + List GetAllForSite(string? appId); - LlmsTxtEntity Get(Guid id); + LlmsTxtEntity? Get(Guid id); void Save(SaveLlmsModel model); diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentService.cs b/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentService.cs index 653ed50..2145465 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/ILlmsContentService.cs @@ -5,15 +5,15 @@ namespace Stott.Optimizely.RobotsHandler.Llms; public interface ILlmsContentService { - IList GetAll(); + IList GetAll(); - SiteLlmsViewModel Get(Guid id); + ApplicationLlmsViewModel Get(Guid id); - SiteLlmsViewModel GetDefault(Guid siteId); + ApplicationLlmsViewModel GetDefault(string? appId); - string GetLlmsContent(Guid siteId, string host); + string? GetLlmsContent(string? appId, string? host); - string GetDefaultLlmsContent(); + string? GetDefaultLlmsContent(); void Save(SaveLlmsModel model); diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsApiController.cs b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsApiController.cs index 9b45fe4..4c88bd3 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsApiController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsApiController.cs @@ -11,27 +11,17 @@ namespace Stott.Optimizely.RobotsHandler.Llms; [ApiExplorerSettings(IgnoreApi = true)] [Authorize(Policy = RobotsConstants.AuthorizationPolicy)] -public sealed class LlmsApiController : BaseApiController +public sealed class LlmsApiController( + ILlmsContentService service, + ILogger logger) : BaseApiController { - private readonly ILlmsContentService _service; - - private readonly ILogger _logger; - - public LlmsApiController( - ILlmsContentService service, - ILogger logger) - { - _service = service; - _logger = logger; - } - [HttpGet] [Route("/stott.robotshandler/api/llms/list/")] public IActionResult ApiList() { var model = new LlmsListViewModel { - List = _service.GetAll() + List = service.GetAll() }; return CreateSafeJsonResult(model); @@ -39,19 +29,19 @@ public IActionResult ApiList() [HttpGet] [Route("/stott.robotshandler/api/llms/[action]")] - public IActionResult Details(string id, string siteId) + public IActionResult Details(string id, string appId) { if (!Guid.TryParse(id, out var llmsId)) { throw new ArgumentException("Id cannot be parsed as a valid GUID.", nameof(id)); } - if (!Guid.TryParse(siteId, out var llmsSiteId) || Guid.Empty.Equals(llmsSiteId)) + if (string.IsNullOrWhiteSpace(appId)) { - throw new ArgumentException("SiteId cannot be parsed as a valid GUID.", nameof(siteId)); + throw new ArgumentException("AppId has not been provided.", nameof(appId)); } - var model = Guid.Empty.Equals(llmsId) ? _service.GetDefault(llmsSiteId) : _service.Get(llmsId); + var model = Guid.Empty.Equals(llmsId) ? service.GetDefault(appId) : service.Get(llmsId); return CreateSafeJsonResult(model); } @@ -62,7 +52,7 @@ public IActionResult Save(SaveLlmsModel formSubmitModel) { try { - if (_service.DoesConflictExists(formSubmitModel)) + if (service.DoesConflictExists(formSubmitModel)) { return new ContentResult { @@ -71,13 +61,13 @@ public IActionResult Save(SaveLlmsModel formSubmitModel) ContentType = "text/plain" }; } - _service.Save(formSubmitModel); + service.Save(formSubmitModel); return new OkResult(); } catch (Exception exception) { - _logger.LogError(exception, "Failed to save llms.txt content for {siteName}", formSubmitModel.SiteName); + logger.LogError(exception, "Failed to save llms.txt content for {siteName}", formSubmitModel.AppName); return new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, @@ -103,13 +93,13 @@ public IActionResult Delete(Guid id) }; } - _service.Delete(id); + service.Delete(id); return new OkResult(); } catch (Exception exception) { - _logger.LogError(exception, "Failed to delete this llms configuration."); + logger.LogError(exception, "Failed to delete this llms configuration."); return new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsListViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsListViewModel.cs index e12dfaa..ef6219d 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsListViewModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsListViewModel.cs @@ -4,5 +4,5 @@ namespace Stott.Optimizely.RobotsHandler.Llms; public sealed class LlmsListViewModel { - public IList List { get; set; } + public IList List { get; set; } = []; } diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsTextController.cs b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsTextController.cs index 84e5dfb..4b5f85a 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/LlmsTextController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/LlmsTextController.cs @@ -1,6 +1,7 @@ using System; +using System.Threading.Tasks; -using EPiServer.Web; +using EPiServer.Applications; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -10,30 +11,23 @@ namespace Stott.Optimizely.RobotsHandler.Llms; -public sealed class LlmsTextController : Controller +public sealed class LlmsTextController( + IApplicationResolver applicationResolver, + ILlmsContentService service, + ILogger logger) : Controller { - private readonly ILlmsContentService _service; - - private readonly ILogger _logger; - - public LlmsTextController(ILlmsContentService service, ILogger logger) - { - _service = service; - _logger = logger; - } - [HttpGet] [Route("llms.txt")] [AllowAnonymous] - public IActionResult Index() + public async Task Index() { try { - var llmsContent = _service.GetLlmsContent(SiteDefinition.Current.Id, Request.Host.Value); - + var application = await applicationResolver.GetByContextAsync(); + var llmsContent = service.GetLlmsContent(application?.Name, Request?.Host.Value); if (string.IsNullOrWhiteSpace(llmsContent)) { - _logger.LogWarning("The llms.txt content is empty for the current site."); + logger.LogWarning("The llms.txt content is empty for the current site."); return NotFound(); } @@ -49,7 +43,7 @@ public IActionResult Index() } catch (Exception exception) { - _logger.LogError(exception, "Failed to load the llms.txt for the current site."); + logger.LogError(exception, "Failed to load the llms.txt for the current site."); throw; } } diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/SaveLlmsModel.cs b/src/Stott.Optimizely.RobotsHandler/Llms/SaveLlmsModel.cs index 3f4d8ad..8da2bf8 100644 --- a/src/Stott.Optimizely.RobotsHandler/Llms/SaveLlmsModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Llms/SaveLlmsModel.cs @@ -6,11 +6,11 @@ public sealed class SaveLlmsModel { public Guid Id { get; set; } - public Guid SiteId { get; set; } + public string? AppId { get; set; } - public string SiteName { get; set; } + public string? AppName { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string LlmsContent { get; set; } + public string? LlmsContent { get; set; } } diff --git a/src/Stott.Optimizely.RobotsHandler/Llms/SiteLlmsViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Llms/SiteLlmsViewModel.cs deleted file mode 100644 index 30dab26..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Llms/SiteLlmsViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -using Stott.Optimizely.RobotsHandler.Common; -using Stott.Optimizely.RobotsHandler.Sites; - -namespace Stott.Optimizely.RobotsHandler.Llms -{ - public sealed class SiteLlmsViewModel : ISiteContentViewModel - { - public Guid Id { get; set; } - - public Guid SiteId { get; set; } - - public string SiteName { get; set; } - - public List AvailableHosts { get; set; } - - public bool IsForWholeSite { get; set; } - - public string SpecificHost { get; set; } - - public string LlmsContent { get; set; } - } -} diff --git a/src/Stott.Optimizely.RobotsHandler/Models/EnvironmentRobotsEntity.cs b/src/Stott.Optimizely.RobotsHandler/Models/EnvironmentRobotsEntity.cs index 5e63140..5f3d4e1 100644 --- a/src/Stott.Optimizely.RobotsHandler/Models/EnvironmentRobotsEntity.cs +++ b/src/Stott.Optimizely.RobotsHandler/Models/EnvironmentRobotsEntity.cs @@ -1,4 +1,5 @@ -using EPiServer.Data; +using System; +using EPiServer.Data; using EPiServer.Data.Dynamic; namespace Stott.Optimizely.RobotsHandler.Models; @@ -6,9 +7,9 @@ namespace Stott.Optimizely.RobotsHandler.Models; [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)] public class EnvironmentRobotsEntity : IDynamicData { - public Identity Id { get; set; } + public Identity Id { get; set; } = Identity.NewIdentity(Guid.NewGuid()); - public string EnvironmentName { get; set; } + public string? EnvironmentName { get; set; } public bool UseNoFollow { get; set; } diff --git a/src/Stott.Optimizely.RobotsHandler/Models/LlmsTxtEntity.cs b/src/Stott.Optimizely.RobotsHandler/Models/LlmsTxtEntity.cs index ea05b57..4fb380a 100644 --- a/src/Stott.Optimizely.RobotsHandler/Models/LlmsTxtEntity.cs +++ b/src/Stott.Optimizely.RobotsHandler/Models/LlmsTxtEntity.cs @@ -8,13 +8,13 @@ namespace Stott.Optimizely.RobotsHandler.Models; [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)] public class LlmsTxtEntity : IDynamicData { - public Identity Id { get; set; } + public Identity Id { get; set; } = Identity.NewIdentity(Guid.NewGuid()); - public Guid SiteId { get; set; } + public string? AppId { get; set; } public bool IsForWholeSite { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string LlmsContent { get; set; } + public string? LlmsContent { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Models/OpalTokenEntity.cs b/src/Stott.Optimizely.RobotsHandler/Models/OpalTokenEntity.cs index d1607b9..8b4b8d8 100644 --- a/src/Stott.Optimizely.RobotsHandler/Models/OpalTokenEntity.cs +++ b/src/Stott.Optimizely.RobotsHandler/Models/OpalTokenEntity.cs @@ -7,17 +7,17 @@ namespace Stott.Optimizely.RobotsHandler.Models; [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)] public class OpalTokenEntity : IDynamicData { - public Identity Id { get; set; } + public Identity Id { get; set; } = Identity.NewIdentity(Guid.NewGuid()); - public string Name { get; set; } + public string? Name { get; set; } - public string RobotsScope { get; set; } + public string? RobotsScope { get; set; } - public string LlmsScope { get; set; } + public string? LlmsScope { get; set; } - public string TokenHash { get; set; } + public string? TokenHash { get; set; } - public string DisplayToken { get; set; } + public string? DisplayToken { get; set; } - public string TokenSalt { get; set; } + public string TokenSalt { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntity.cs b/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntity.cs index 65fd46c..1bc10e4 100644 --- a/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntity.cs +++ b/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntity.cs @@ -8,13 +8,13 @@ namespace Stott.Optimizely.RobotsHandler.Models; [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)] public class RobotsEntity : IDynamicData { - public Identity Id { get; set; } + public Identity Id { get; set; } = Identity.NewIdentity(Guid.NewGuid()); - public Guid SiteId { get; set; } + public string? AppId { get; set; } public bool IsForWholeSite { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string RobotsContent { get; set; } + public string? RobotsContent { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntityNotFoundException.cs b/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntityNotFoundException.cs index 2e390aa..c39980f 100644 --- a/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntityNotFoundException.cs +++ b/src/Stott.Optimizely.RobotsHandler/Models/RobotsEntityNotFoundException.cs @@ -1,7 +1,7 @@ using System; -using System.Runtime.Serialization; -[Serializable] +namespace Stott.Optimizely.RobotsHandler.Models; + public class RobotsEntityNotFoundException : Exception { public RobotsEntityNotFoundException() @@ -22,9 +22,4 @@ public RobotsEntityNotFoundException(string message, Exception inner) : base(message, inner) { } - - protected RobotsEntityNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/IOpalTokenRepository.cs b/src/Stott.Optimizely.RobotsHandler/Opal/IOpalTokenRepository.cs index c6a7107..a3d0ee5 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/IOpalTokenRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/IOpalTokenRepository.cs @@ -11,5 +11,5 @@ public interface IOpalTokenRepository void Delete(Guid id); - TokenModel GetByToken(string token); + TokenModel? GetByToken(string token); } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/Function.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/Function.cs index f1734cc..34223bd 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/Function.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/Function.cs @@ -6,17 +6,17 @@ namespace Stott.Optimizely.RobotsHandler.Opal.Models; public class Function { [JsonPropertyName("name")] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("description")] - public string Description { get; set; } + public string? Description { get; set; } [JsonPropertyName("parameters")] - public List Parameters { get; set; } + public List Parameters { get; set; } = []; [JsonPropertyName("endpoint")] - public string Endpoint { get; set; } + public string? Endpoint { get; set; } [JsonPropertyName("http_method")] - public string HttpMethod { get; set; } + public string? HttpMethod { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionParameter.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionParameter.cs index 3f2662d..f4a30a3 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionParameter.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionParameter.cs @@ -5,13 +5,13 @@ namespace Stott.Optimizely.RobotsHandler.Opal.Models; public class FunctionParameter { [JsonPropertyName("name")] - public string Name { get; set; } + public string? Name { get; set; } [JsonPropertyName("type")] - public string Type { get; set; } + public string? Type { get; set; } [JsonPropertyName("description")] - public string Description { get; set; } + public string? Description { get; set; } [JsonPropertyName("required")] public bool Required { get; set; } diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionsRoot.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionsRoot.cs index 419a28d..a9caabf 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionsRoot.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/FunctionsRoot.cs @@ -6,5 +6,5 @@ namespace Stott.Optimizely.RobotsHandler.Opal.Models; public class FunctionsRoot { [JsonPropertyName("functions")] - public List Functions { get; set; } + public List Functions { get; set; } = []; } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/GetConfigurationsQuery.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/GetConfigurationsQuery.cs index d415221..f876ff5 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/GetConfigurationsQuery.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/GetConfigurationsQuery.cs @@ -2,5 +2,5 @@ public class GetConfigurationsQuery { - public string HostName { get; set; } + public string? HostName { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/OpalSiteContentModel.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/OpalSiteContentModel.cs index 32c2140..059b53b 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/OpalSiteContentModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/OpalSiteContentModel.cs @@ -6,9 +6,9 @@ public class OpalSiteContentModel { public Guid Id { get; set; } - public string SiteName { get; set; } + public string? SiteName { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string Content { get; set; } + public string? Content { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveLlmsTextConfigurationsCommand.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveLlmsTextConfigurationsCommand.cs index a3e4e94..94e992e 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveLlmsTextConfigurationsCommand.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveLlmsTextConfigurationsCommand.cs @@ -2,9 +2,9 @@ public class SaveLlmsTextConfigurationsCommand { - public string LlmsId { get; set; } + public string? LlmsId { get; set; } - public string HostName { get; set; } + public string? HostName { get; set; } - public string LlmsTxtContent { get; set; } + public string? LlmsTxtContent { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveRobotTextConfigurationsCommand.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveRobotTextConfigurationsCommand.cs index 1a556f4..779466a 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveRobotTextConfigurationsCommand.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/SaveRobotTextConfigurationsCommand.cs @@ -2,9 +2,9 @@ public class SaveRobotTextConfigurationsCommand { - public string RobotsId { get; set; } + public string? RobotsId { get; set; } - public string HostName { get; set; } + public string? HostName { get; set; } - public string RobotsTxtContent { get; set; } + public string? RobotsTxtContent { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/Models/ToolRequest.cs b/src/Stott.Optimizely.RobotsHandler/Opal/Models/ToolRequest.cs index aa7a679..317184a 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/Models/ToolRequest.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/Models/ToolRequest.cs @@ -2,5 +2,5 @@ public class ToolRequest where TModel : class { - public TModel Parameters { get; set; } + public TModel? Parameters { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/OpalBaseApiController.cs b/src/Stott.Optimizely.RobotsHandler/Opal/OpalBaseApiController.cs index 0e4125f..5f09f68 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/OpalBaseApiController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/OpalBaseApiController.cs @@ -2,25 +2,18 @@ using System.Collections.Generic; using System.Linq; -using EPiServer.Web; +using EPiServer.Applications; +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Common; -using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Opal.Models; namespace Stott.Optimizely.RobotsHandler.Opal; -public abstract class OpalBaseApiController : BaseApiController +public abstract class OpalBaseApiController(IApplicationResolver applicationResolver) : BaseApiController { - private readonly ISiteDefinitionResolver _siteDefinitionResolver; - - protected OpalBaseApiController(ISiteDefinitionResolver siteDefinitionResolver) - { - _siteDefinitionResolver = siteDefinitionResolver; - } - - protected static IEnumerable ConvertToModels(IList siteModels, Func contentSelector) - where TContent : ISiteContentViewModel + protected static IEnumerable ConvertToModels(IList siteModels, Func contentSelector) + where TContent : IApplicationContentViewModel { if (siteModels is null) { @@ -37,14 +30,14 @@ protected static IEnumerable ConvertToModels(ILi { Id = siteModel.Id, SpecificHost = siteModel.SpecificHost, - SiteName = siteModel.SiteName, + SiteName = siteModel.AppName, Content = contentSelector(siteModel) }; continue; } - var availableHosts = siteModel.AvailableHosts.Where(x => !string.IsNullOrWhiteSpace(x.HostName)).Select(x => x.HostName).ToList(); + var availableHosts = siteModel.AvailableHosts?.Where(x => !string.IsNullOrWhiteSpace(x.HostName)).Select(x => x.HostName!).ToList() ?? []; foreach (var availableHost in availableHosts) { if (!specifiedHosts.Any(x => string.Equals(x, availableHost, StringComparison.OrdinalIgnoreCase))) @@ -53,7 +46,7 @@ protected static IEnumerable ConvertToModels(ILi { Id = siteModel.Id, SpecificHost = availableHost, - SiteName = siteModel.SiteName, + SiteName = siteModel.AppName, Content = contentSelector(siteModel) }; } @@ -61,46 +54,61 @@ protected static IEnumerable ConvertToModels(ILi } } - protected static OpalSiteContentModel ConvertToModel(TContent viewModel, string specificHost, Func contentSelector) - where TContent : ISiteContentViewModel + protected static OpalSiteContentModel ConvertToModel(TContent viewModel, string specificHost, Func contentSelector) + where TContent : IApplicationContentViewModel { return new OpalSiteContentModel { Id = viewModel.Id, SpecificHost = specificHost, - SiteName = viewModel.SiteName, + SiteName = viewModel.AppName, Content = contentSelector(viewModel) }; } - protected TModel GetEmptySiteModel(string hostName) - where TModel : ISiteContentViewModel + protected TModel? GetEmptySiteModel(string hostName) + where TModel : IApplicationContentViewModel { - var siteDefinition = _siteDefinitionResolver.GetByHostname(hostName, false, out var matchedHost); - if (siteDefinition is null) + var applicationDefinition = applicationResolver.GetByHostname(hostName, false); + if (applicationDefinition?.Application is null) { return default; } var model = Activator.CreateInstance(); model.Id = Guid.Empty; - model.SiteId = siteDefinition.Id; + model.AppId = applicationDefinition.Application?.Name; model.IsForWholeSite = false; model.SpecificHost = hostName; - model.SiteName = siteDefinition.Name; - model.AvailableHosts = siteDefinition.Hosts.ToHostSummaries().ToList(); + model.AppName = applicationDefinition.Application?.DisplayName; + model.AvailableHosts = [.. GetHostViewModels(applicationDefinition.Application)]; return model; } - private static IList GetSpecificHosts(IList models) - where TContent : ISiteContentViewModel + private static List GetSpecificHosts(IList models) + where TContent : IApplicationContentViewModel { if (models is null) { - return new List(); + return []; + } + + return [.. models.Where(x => !string.IsNullOrWhiteSpace(x.SpecificHost)).Select(x => x.SpecificHost!)]; + } + + private static IEnumerable GetHostViewModels(Application? application) + { + if (application is Website website) + { + return ApplicationMapper.CreateHostSummaries(website.Hosts); + } + + if (application is InProcessWebsite inprocessWebsite) + { + return ApplicationMapper.CreateHostSummaries(inprocessWebsite.Hosts); } - return models.Where(x => !string.IsNullOrWhiteSpace(x.SpecificHost)).Select(x => x.SpecificHost).ToList(); + return []; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/OpalLlmsApiController.cs b/src/Stott.Optimizely.RobotsHandler/Opal/OpalLlmsApiController.cs index 9ff8a94..b2c9c93 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/OpalLlmsApiController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/OpalLlmsApiController.cs @@ -1,33 +1,24 @@ using System; using System.Linq; -using EPiServer.Web; +using EPiServer.Applications; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Llms; using Stott.Optimizely.RobotsHandler.Opal.Models; namespace Stott.Optimizely.RobotsHandler.Opal; [AllowAnonymous] -public sealed class OpalLlmsApiController : OpalBaseApiController +public sealed class OpalLlmsApiController( + ILlmsContentService service, + IApplicationResolver applicationResolver, + ILogger logger) : OpalBaseApiController(applicationResolver) { - private readonly ILlmsContentService _service; - - private readonly ILogger _logger; - - public OpalLlmsApiController( - ILlmsContentService service, - ISiteDefinitionResolver siteDefinitionResolver, - ILogger logger) : base(siteDefinitionResolver) - { - _service = service; - _logger = logger; - } - [HttpPost] [Route("/stott.robotshandler/opal/tools/get-llms-txt-configurations/")] [Route("/stott.robotshandler/opal/discovery/tools/get-llms-txt-configurations/")] @@ -36,10 +27,11 @@ public IActionResult GetLlmsTxtConfigurations([FromBody] ToolRequest string.Equals(x.SpecificHost, hostName, StringComparison.OrdinalIgnoreCase)) ?? configurations.FirstOrDefault(x => x.AvailableHosts.Any(h => string.Equals(h.HostName, hostName, StringComparison.OrdinalIgnoreCase))); @@ -49,7 +41,7 @@ public IActionResult GetLlmsTxtConfigurations([FromBody] ToolRequest Equals(x.Id, llmsId)); if (specificRobotsConfig is null) @@ -100,13 +92,13 @@ public IActionResult SaveLlmsTxtConfigurations([FromBody] ToolRequest string.Equals(x.SpecificHost, hostName, StringComparison.OrdinalIgnoreCase)) ?? configurations.FirstOrDefault(x => x.AvailableHosts.Any(h => string.Equals(h.HostName, hostName, StringComparison.OrdinalIgnoreCase))) ?? - GetEmptySiteModel(hostName); + GetEmptySiteModel(hostName); if (specificConfiguration is null) { return Json(new { Success = false, - Message = $"Could not locate a site that matched the host name of {model.Parameters.HostName}." + Message = $"Could not locate a site that matched the host name of {hostName}." }); } @@ -135,13 +127,13 @@ public IActionResult SaveLlmsTxtConfigurations([FromBody] ToolRequest logger) : OpalBaseApiController(applicationResolver) { - private readonly IRobotsContentService _service; - - private readonly ILogger _logger; - - public OpalRobotsApiController( - IRobotsContentService service, - ISiteDefinitionResolver siteDefinitionResolver, - ILogger logger) : base(siteDefinitionResolver) - { - _service = service; - _logger = logger; - } - [HttpPost] [Route("/stott.robotshandler/opal/tools/get-robot-txt-configurations/")] [Route("/stott.robotshandler/opal/discovery/tools/get-robot-txt-configurations/")] @@ -36,10 +27,11 @@ public IActionResult GetRobotTxtConfigurations([FromBody] ToolRequest string.Equals(x.SpecificHost, hostName, StringComparison.OrdinalIgnoreCase)) ?? configurations.FirstOrDefault(x => x.AvailableHosts.Any(h => string.Equals(h.HostName, hostName, StringComparison.OrdinalIgnoreCase))); @@ -49,7 +41,7 @@ public IActionResult GetRobotTxtConfigurations([FromBody] ToolRequest List() { var records = store.Find(new Dictionary()).ToList(); - return records.Select(ToModel).ToList(); + return [.. records.Select(ToModel).Where(model => model != null).Cast()]; } public void Save(TokenModel saveModel) @@ -52,25 +45,25 @@ public void Save(TokenModel saveModel) if (!string.IsNullOrWhiteSpace(saveModel.Token)) { - recordToSave.TokenHash = _hashService.HashToken(saveModel.Token, recordToSave.TokenSalt); + recordToSave.TokenHash = hashService.HashToken(saveModel.Token, recordToSave.TokenSalt); recordToSave.DisplayToken = saveModel.Token.Length > 4 ? $"{saveModel.Token[..4]}..." : saveModel.Token; } store.Save(recordToSave); } - public TokenModel GetByToken(string token) + public TokenModel? GetByToken(string token) { if (string.IsNullOrWhiteSpace(token)) return null; var allTokens = store.Find(new Dictionary()).ToList(); - var matchingHashedToken = allTokens.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t.TokenHash) && _hashService.VerifyToken(token, t.TokenHash, t.TokenSalt)); + var matchingHashedToken = allTokens.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t.TokenHash) && hashService.VerifyToken(token, t.TokenHash, t.TokenSalt)); return ToModel(matchingHashedToken); } - private static TokenModel ToModel(OpalTokenEntity entity) + private static TokenModel? ToModel(OpalTokenEntity? entity) { if (entity is null) { @@ -87,7 +80,7 @@ private static TokenModel ToModel(OpalTokenEntity entity) }; } - private OpalTokenEntity Get(Guid id) + private OpalTokenEntity? Get(Guid id) { if (Guid.Empty.Equals(id)) { diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/TokenHashService.cs b/src/Stott.Optimizely.RobotsHandler/Opal/TokenHashService.cs index eba6ea0..05659ac 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/TokenHashService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/TokenHashService.cs @@ -20,11 +20,8 @@ public string HashToken(string token, string saltValue) } // Hash the token with the date-based salt using PBKDF2 - using (var pbkdf2 = new Rfc2898DeriveBytes(token, ToByteArray(saltValue), Iterations, HashAlgorithmName.SHA256)) - { - var hash = pbkdf2.GetBytes(HashSize); - return Convert.ToBase64String(hash); - } + var hash = Rfc2898DeriveBytes.Pbkdf2(token, ToByteArray(saltValue), Iterations, HashAlgorithmName.SHA256, HashSize); + return Convert.ToBase64String(hash); } public bool VerifyToken(string token, string hash, string saltValue) diff --git a/src/Stott.Optimizely.RobotsHandler/Opal/TokenModel.cs b/src/Stott.Optimizely.RobotsHandler/Opal/TokenModel.cs index 6216df4..4c55676 100644 --- a/src/Stott.Optimizely.RobotsHandler/Opal/TokenModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Opal/TokenModel.cs @@ -6,13 +6,13 @@ public class TokenModel { public Guid Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } - public string RobotsScope { get; set; } + public string? RobotsScope { get; set; } - public string LlmsScope { get; set; } + public string? LlmsScope { get; set; } - public string Token { get; set; } + public string? Token { get; set; } /// /// When the token was created - used as salt for deterministic hashing diff --git a/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/LandingPageViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/LandingPageViewModel.cs index cab7574..657387c 100644 --- a/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/LandingPageViewModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/LandingPageViewModel.cs @@ -2,13 +2,13 @@ public sealed class LandingPageViewModel { - public string ApplicationName { get; internal set; } + public string? ApplicationName { get; internal set; } - public string ApplicationVersion { get; internal set; } + public string? ApplicationVersion { get; internal set; } - public string JavaScriptFile { get; internal set; } + public string? JavaScriptFile { get; internal set; } - public string CssFile { get; internal set; } + public string? CssFile { get; internal set; } - public string Nonce { get; internal set; } + public string? Nonce { get; internal set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/RobotsLandingPageController.cs b/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/RobotsLandingPageController.cs index 59a63f4..6afbbd7 100644 --- a/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/RobotsLandingPageController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Presentation/LandingPage/RobotsLandingPageController.cs @@ -12,18 +12,8 @@ using Stott.Security.Optimizely.Features.StaticFile; [Authorize(Policy = RobotsConstants.AuthorizationPolicy)] -public sealed class RobotsLandingPageController : Controller +public sealed class RobotsLandingPageController(ICspNonceService cspNonceService, IStaticFileResolver staticFileResolver) : Controller { - private readonly ICspNonceService _cspNonceService; - - private readonly IStaticFileResolver _staticFileResolver; - - public RobotsLandingPageController(ICspNonceService cspNonceService, IStaticFileResolver staticFileResolver) - { - _cspNonceService = cspNonceService; - _staticFileResolver = staticFileResolver; - } - [HttpGet] [Route("/stott.robotshandler/administration/")] public IActionResult Index() @@ -32,8 +22,8 @@ public IActionResult Index() { ApplicationName = "Stott Robots Handler", ApplicationVersion = GetApplicationVersion(), - JavaScriptFile = $"/stott.robotshandler/static/{_staticFileResolver.GetJavaScriptFileName()}", - CssFile = $"/stott.robotshandler/static/{_staticFileResolver.GetStyleSheetFileName()}", + JavaScriptFile = $"/stott.robotshandler/static/{staticFileResolver.GetJavaScriptFileName()}", + CssFile = $"/stott.robotshandler/static/{staticFileResolver.GetStyleSheetFileName()}", Nonce = GetNonce() }; @@ -44,8 +34,8 @@ public IActionResult Index() [Route("/stott.robotshandler/static/{staticFileName}")] public IActionResult ApplicationStaticFile(string staticFileName) { - var fileBytes = _staticFileResolver.GetFileContent(staticFileName); - var mimeType = _staticFileResolver.GetFileMimeType(staticFileName); + var fileBytes = staticFileResolver.GetFileContent(staticFileName); + var mimeType = staticFileResolver.GetFileMimeType(staticFileName); if (fileBytes.Length == 0) { @@ -63,11 +53,11 @@ private static string GetApplicationVersion() return $"v{assemblyName?.Version}"; } - private string GetNonce() + private string? GetNonce() { try { - return _cspNonceService.GetNonce(); + return cspNonceService.GetNonce(); } catch (Exception) { diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentRepository.cs b/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentRepository.cs index 7ef16b6..240c10a 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentRepository.cs @@ -9,9 +9,9 @@ public interface IRobotsContentRepository { List GetAll(); - List GetAllForSite(Guid siteId); + List GetAllForSite(string? appId); - RobotsEntity Get(Guid id); + RobotsEntity? Get(Guid id); void Save(SaveRobotsModel model); diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentService.cs b/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentService.cs index 6ded34c..574c850 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/IRobotsContentService.cs @@ -9,9 +9,9 @@ public interface IRobotsContentService SiteRobotsViewModel Get(Guid id); - SiteRobotsViewModel GetDefault(Guid siteId); + SiteRobotsViewModel GetDefault(string? appId); - string GetRobotsContent(Guid siteId, string host); + string? GetRobotsContent(string? appId, string? host); string GetDefaultRobotsContent(); diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsApiController.cs b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsApiController.cs index 88b7334..8f47257 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsApiController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsApiController.cs @@ -11,27 +11,17 @@ [ApiExplorerSettings(IgnoreApi = true)] [Authorize(Policy = RobotsConstants.AuthorizationPolicy)] -public sealed class RobotsApiController : BaseApiController +public sealed class RobotsApiController( + IRobotsContentService service, + ILogger logger) : BaseApiController { - private readonly IRobotsContentService _service; - - private readonly ILogger _logger; - - public RobotsApiController( - IRobotsContentService service, - ILogger logger) - { - _service = service; - _logger = logger; - } - [HttpGet] [Route("/stott.robotshandler/api/robots/list/")] public IActionResult ApiList() { var model = new RobotsListViewModel { - List = _service.GetAll() + List = service.GetAll() }; return CreateSafeJsonResult(model); @@ -39,19 +29,19 @@ public IActionResult ApiList() [HttpGet] [Route("/stott.robotshandler/api/robots/[action]")] - public IActionResult Details(string id, string siteId) + public IActionResult Details(string? id, string? appId) { if (!Guid.TryParse(id, out var robotsId)) { throw new ArgumentException("Id cannot be parsed as a valid GUID.", nameof(id)); } - if (!Guid.TryParse(siteId, out var robotsSiteId) || Guid.Empty.Equals(robotsSiteId)) + if (string.IsNullOrWhiteSpace(appId)) { - throw new ArgumentException("SiteId cannot be parsed as a valid GUID.", nameof(siteId)); + throw new ArgumentException("AppId has not been provided.", nameof(appId)); } - var model = Guid.Empty.Equals(robotsId) ? _service.GetDefault(robotsSiteId) : _service.Get(robotsId); + var model = Guid.Empty.Equals(robotsId) ? service.GetDefault(appId) : service.Get(robotsId); return CreateSafeJsonResult(model); } @@ -62,7 +52,7 @@ public IActionResult Save(SaveRobotsModel formSubmitModel) { try { - if (_service.DoesConflictExists(formSubmitModel)) + if (service.DoesConflictExists(formSubmitModel)) { return new ContentResult { @@ -71,13 +61,13 @@ public IActionResult Save(SaveRobotsModel formSubmitModel) ContentType = "text/plain" }; } - _service.Save(formSubmitModel); + service.Save(formSubmitModel); return new OkResult(); } catch (Exception exception) { - _logger.LogError(exception, "Failed to save robots.txt content for {siteName}", formSubmitModel.SiteName); + logger.LogError(exception, "Failed to save robots.txt content for {siteName}", formSubmitModel.AppName); return new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, @@ -103,13 +93,13 @@ public IActionResult Delete(Guid id) }; } - _service.Delete(id); + service.Delete(id); return new OkResult(); } catch (Exception exception) { - _logger.LogError(exception, "Failed to delete this robots configuration."); + logger.LogError(exception, "Failed to delete this robots configuration."); return new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentRepository.cs b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentRepository.cs index e5c1ac8..eb09524 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentRepository.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentRepository.cs @@ -5,6 +5,7 @@ using EPiServer.Data; using EPiServer.Data.Dynamic; +using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Models; namespace Stott.Optimizely.RobotsHandler.Robots; @@ -18,7 +19,7 @@ public RobotsContentRepository() store = DynamicDataStoreFactory.Instance.CreateStore(typeof(RobotsEntity)); } - public RobotsEntity Get(Guid id) + public RobotsEntity? Get(Guid id) { if (Guid.Empty.Equals(id)) { @@ -33,21 +34,23 @@ public List GetAll() return store.Find(new Dictionary()).ToList(); } - public List GetAllForSite(Guid siteId) + public List GetAllForSite(string? appId) { - return store.Find(new Dictionary { { nameof(RobotsEntity.SiteId), siteId } }).ToList(); + if (string.IsNullOrWhiteSpace(appId)) + { + return []; + } + + return store.Find(new Dictionary { { nameof(RobotsEntity.AppId), appId } }).ToList(); } public void Save(SaveRobotsModel model) { var recordToSave = Get(model.Id); - recordToSave ??= new RobotsEntity - { - Id = Identity.NewIdentity(Guid.NewGuid()), - SiteId = model.SiteId, - }; + recordToSave ??= new RobotsEntity { Id = Identity.NewIdentity(Guid.NewGuid()) }; - recordToSave.SpecificHost = model.SpecificHost; + recordToSave.AppId = model.AppId; + recordToSave.SpecificHost = model.SpecificHost.GetSanitizedHostDomain(); recordToSave.IsForWholeSite = string.IsNullOrWhiteSpace(model.SpecificHost); recordToSave.RobotsContent = model.RobotsContent; diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentService.cs b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentService.cs index 2b1dd82..f728e8d 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentService.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsContentService.cs @@ -3,27 +3,16 @@ using System.Linq; using System.Text; -using EPiServer.Web; - +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Extensions; using Stott.Optimizely.RobotsHandler.Models; namespace Stott.Optimizely.RobotsHandler.Robots; -public sealed class RobotsContentService : IRobotsContentService +public sealed class RobotsContentService( + IApplicationDefinitionService appService, + IRobotsContentRepository robotsContentRepository) : IRobotsContentService { - private readonly ISiteDefinitionRepository siteDefinitionRepository; - - private readonly IRobotsContentRepository robotsContentRepository; - - public RobotsContentService( - ISiteDefinitionRepository siteDefinitionRepository, - IRobotsContentRepository robotsContentRepository) - { - this.siteDefinitionRepository = siteDefinitionRepository; - this.robotsContentRepository = robotsContentRepository; - } - public string GetDefaultRobotsContent() { var stringBuilder = new StringBuilder(); @@ -36,27 +25,27 @@ public string GetDefaultRobotsContent() public IList GetAll() { var robotRecords = robotsContentRepository.GetAll(); - var sites = siteDefinitionRepository.List(); + var applications = appService.GetAllApplicationsAsync().GetAwaiter().GetResult(); var models = new List(); foreach (var robotRecord in robotRecords) { - var site = sites.FirstOrDefault(x => x.Id.Equals(robotRecord.SiteId)); + var site = applications.FirstOrDefault(x => string.Equals(x.AppId, robotRecord.AppId, StringComparison.OrdinalIgnoreCase)); if (site != null) { models.Add(ToModel(robotRecord, site)); } } - foreach (var site in sites) + foreach (var site in applications) { - if (!models.Any(x => x.SiteId == site.Id && x.IsForWholeSite)) + if (!models.Any(x => string.Equals(x.AppId, site.AppId, StringComparison.OrdinalIgnoreCase) && x.IsForWholeSite)) { models.Add(ToModel(site)); } } - return models.OrderBy(x => x.SiteName).ThenBy(x => x.SpecificHost).ToList(); + return models.OrderBy(x => x.AppName).ThenBy(x => x.SpecificHost).ToList(); } public SiteRobotsViewModel Get(Guid id) @@ -67,31 +56,32 @@ public SiteRobotsViewModel Get(Guid id) throw new RobotsEntityNotFoundException(id); } - var sites = siteDefinitionRepository.List(); - var site = sites.FirstOrDefault(x => x.Id.Equals(robotRecord.SiteId)); - if (site == null) + var applications = appService.GetAllApplicationsAsync().GetAwaiter().GetResult(); + var application = applications.FirstOrDefault(x => string.Equals(x.AppId, robotRecord.AppId, StringComparison.OrdinalIgnoreCase)); + if (application == null) { - throw new RobotsEntityNotFoundException($"Robotes entity with id '{id}' not match a site definition."); + throw new RobotsEntityNotFoundException($"Robotes entity with id '{id}' not match an application definition."); } - return ToModel(robotRecord, site); + return ToModel(robotRecord, application); } - public SiteRobotsViewModel GetDefault(Guid siteId) + public SiteRobotsViewModel GetDefault(string? appId) { - var site = siteDefinitionRepository.Get(siteId); - if (site == null) + var application = appService.GetApplicationByIdAsync(appId).GetAwaiter().GetResult(); + if (application == null) { - throw new ArgumentException($"{nameof(siteId)} does not correlate to a known site.", nameof(siteId)); + throw new ArgumentException($"{nameof(appId)} does not correlate to a known application.", nameof(appId)); } - return ToModel(site); + return ToModel(application); } - public string GetRobotsContent(Guid siteId, string host) + public string? GetRobotsContent(string? appId, string? host) { - var robots = robotsContentRepository.GetAllForSite(siteId) ?? new List(0); - var matchingRobots = robots.FirstOrDefault(x => string.Equals(x.SpecificHost, host, StringComparison.OrdinalIgnoreCase)) ?? + var cleanedHost = host.GetSanitizedHostDomain(); + var robots = robotsContentRepository.GetAllForSite(appId) ?? []; + var matchingRobots = robots.FirstOrDefault(x => string.Equals(x.SpecificHost, cleanedHost, StringComparison.OrdinalIgnoreCase)) ?? robots.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.SpecificHost)); if (matchingRobots == null) @@ -104,15 +94,15 @@ public string GetRobotsContent(Guid siteId, string host) public void Save(SaveRobotsModel model) { - if (Guid.Empty.Equals(model.SiteId)) + if (Guid.Empty.Equals(model.AppId)) { - throw new ArgumentException($"{nameof(model)}.{nameof(model.SiteId)} must not be null or empty.", nameof(model)); + throw new ArgumentException($"{nameof(model)}.{nameof(model.AppId)} must not be null or empty.", nameof(model)); } - var existingSite = siteDefinitionRepository.Get(model.SiteId); - if (existingSite == null) + var application = appService.GetApplicationByIdAsync(model.AppId).GetAwaiter().GetResult(); + if (application == null) { - throw new ArgumentException($"{nameof(model)}.{nameof(model.SiteId)} does not correlate to a known site.", nameof(model)); + throw new ArgumentException($"{nameof(model)}.{nameof(model.AppId)} does not correlate to a known application.", nameof(model)); } robotsContentRepository.Save(model); @@ -134,40 +124,41 @@ public bool DoesConflictExists(SaveRobotsModel model) return existingConfigurations.Any(x => IsConflict(model, x)); } - private static SiteRobotsViewModel ToModel(RobotsEntity robotsEntity, SiteDefinition siteDefinition) + private static SiteRobotsViewModel ToModel(RobotsEntity robotsEntity, ApplicationViewModel application) { return new SiteRobotsViewModel { Id = robotsEntity.Id.ExternalId, - SiteId = robotsEntity.SiteId, + AppId = robotsEntity.AppId, IsForWholeSite = robotsEntity.IsForWholeSite || string.IsNullOrWhiteSpace(robotsEntity.SpecificHost), SpecificHost = robotsEntity.SpecificHost, RobotsContent = robotsEntity.RobotsContent, - SiteName = siteDefinition.Name, - AvailableHosts = siteDefinition.Hosts.ToHostSummaries().ToList(), + AppName = application.AppName, + AvailableHosts = application.AvailableHosts, CanDelete = true }; } - private SiteRobotsViewModel ToModel(SiteDefinition siteDefinition) + private SiteRobotsViewModel ToModel(ApplicationViewModel application) { return new SiteRobotsViewModel { Id = Guid.Empty, - SiteId = siteDefinition.Id, + AppId = application.AppId, IsForWholeSite = true, RobotsContent = GetDefaultRobotsContent(), - SiteName = siteDefinition.Name, - AvailableHosts = siteDefinition.Hosts.ToHostSummaries().ToList() + AppName = application.AppName, + AvailableHosts = application.AvailableHosts }; } private static bool IsConflict(SaveRobotsModel model, RobotsEntity entity) { - var modelHost = model.SpecificHost ?? string.Empty; - var entityHost = entity.SpecificHost ?? string.Empty; + var modelHost = model.SpecificHost.GetSanitizedHostDomain() ?? string.Empty; + var entityHost = entity.SpecificHost.GetSanitizedHostDomain() ?? string.Empty; - return Equals(model.SiteId, entity.SiteId) && !Equals(model.Id, entity.Id.ExternalId) && + return string.Equals(model.AppId, entity.AppId, StringComparison.OrdinalIgnoreCase) && + !Guid.Equals(model.Id, entity.Id.ExternalId) && string.Equals(modelHost, entityHost, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsListViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsListViewModel.cs index dd929eb..072f6ab 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsListViewModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsListViewModel.cs @@ -4,5 +4,5 @@ public class RobotsListViewModel { - public IList List { get; set; } + public IList List { get; set; } = []; } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsTextController.cs b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsTextController.cs index 0bdeecd..c50b654 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/RobotsTextController.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/RobotsTextController.cs @@ -1,8 +1,8 @@ namespace Stott.Optimizely.RobotsHandler.Robots; using System; - -using EPiServer.Web; +using System.Threading.Tasks; +using EPiServer.Applications; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -10,26 +10,20 @@ using Stott.Optimizely.RobotsHandler.Extensions; -public sealed class RobotsTextController : Controller +public sealed class RobotsTextController( + IApplicationResolver applicationResolver, + IRobotsContentService service, + ILogger logger) : Controller { - private readonly IRobotsContentService _service; - - private readonly ILogger _logger; - - public RobotsTextController(IRobotsContentService service, ILogger logger) - { - _service = service; - _logger = logger; - } - [HttpGet] [Route("robots.txt")] [AllowAnonymous] - public IActionResult Index() + public async Task Index() { try { - var robotsContent = _service.GetRobotsContent(SiteDefinition.Current.Id, Request.Host.Value); + var application = await applicationResolver.GetByContextAsync(); + var robotsContent = service.GetRobotsContent(application?.Name, Request?.Host.Value); // Set a low cache duration, but not zero to ensure the CDN protects against DDOS attacks Response.Headers.AddOrUpdateHeader("Cache-Control", "public, max-age=300"); @@ -43,7 +37,7 @@ public IActionResult Index() } catch (Exception exception) { - _logger.LogError(exception, "Failed to load the robots.txt for the current site."); + logger.LogError(exception, "Failed to load the robots.txt for the current site."); throw; } } diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/SaveRobotsModel.cs b/src/Stott.Optimizely.RobotsHandler/Robots/SaveRobotsModel.cs index 048e9c6..3ae6beb 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/SaveRobotsModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/SaveRobotsModel.cs @@ -6,11 +6,11 @@ public sealed class SaveRobotsModel { public Guid Id { get; set; } - public Guid SiteId { get; set; } + public string? AppId { get; set; } - public string SiteName { get; set; } + public string? AppName { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string RobotsContent { get; set; } + public string? RobotsContent { get; set; } } \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Robots/SiteRobotsViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Robots/SiteRobotsViewModel.cs index fdc42f6..0b49146 100644 --- a/src/Stott.Optimizely.RobotsHandler/Robots/SiteRobotsViewModel.cs +++ b/src/Stott.Optimizely.RobotsHandler/Robots/SiteRobotsViewModel.cs @@ -1,26 +1,26 @@ using System; using System.Collections.Generic; +using Stott.Optimizely.RobotsHandler.Applications; using Stott.Optimizely.RobotsHandler.Common; -using Stott.Optimizely.RobotsHandler.Sites; namespace Stott.Optimizely.RobotsHandler.Robots; -public sealed class SiteRobotsViewModel : ISiteContentViewModel +public sealed class SiteRobotsViewModel : IApplicationContentViewModel { public Guid Id { get; set; } - public Guid SiteId { get; set; } + public string? AppId { get; set; } - public string SiteName { get; set; } + public string? AppName { get; set; } - public List AvailableHosts { get; set; } + public List AvailableHosts { get; set; } = []; public bool IsForWholeSite { get; set; } - public string SpecificHost { get; set; } + public string? SpecificHost { get; set; } - public string RobotsContent { get; set; } + public string? RobotsContent { get; set; } public bool CanDelete { get; set; } } diff --git a/src/Stott.Optimizely.RobotsHandler/Sites/SiteDefinitionController.cs b/src/Stott.Optimizely.RobotsHandler/Sites/SiteDefinitionController.cs deleted file mode 100644 index 265cfce..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Sites/SiteDefinitionController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; - -using EPiServer.Web; - -using Microsoft.AspNetCore.Mvc; - -using Stott.Optimizely.RobotsHandler.Common; -using Stott.Optimizely.RobotsHandler.Extensions; - -namespace Stott.Optimizely.RobotsHandler.Sites; - -public sealed class SiteDefinitionController : BaseApiController -{ - private readonly ISiteDefinitionRepository _siteRepository; - - public SiteDefinitionController(ISiteDefinitionRepository siteRepository) - { - _siteRepository = siteRepository; - } - - [HttpGet] - [Route("/stott.robotshandler/api/[action]")] - public IActionResult Sites() - { - var sites = _siteRepository.List() - .Select(x => new SiteViewModel - { - SiteId = x.Id, - SiteName = x.Name, - AvailableHosts = x.Hosts.ToHostSummaries().ToList() - }) - .ToList(); - - return CreateSafeJsonResult(sites); - } -} diff --git a/src/Stott.Optimizely.RobotsHandler/Sites/SiteHostViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Sites/SiteHostViewModel.cs deleted file mode 100644 index 86344c0..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Sites/SiteHostViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Stott.Optimizely.RobotsHandler.Sites; - -public sealed class SiteHostViewModel -{ - public string DisplayName { get; set; } - - public string HostName { get; set; } -} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Sites/SiteViewModel.cs b/src/Stott.Optimizely.RobotsHandler/Sites/SiteViewModel.cs deleted file mode 100644 index 88bfbca..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Sites/SiteViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Stott.Optimizely.RobotsHandler.Sites; - -public sealed class SiteViewModel -{ - public Guid SiteId { get; set; } - - public string SiteName { get; set; } - - public List AvailableHosts { get; set; } -} \ No newline at end of file diff --git a/src/Stott.Optimizely.RobotsHandler/Static/index-RUgWUs8j.js b/src/Stott.Optimizely.RobotsHandler/Static/index-RUgWUs8j.js deleted file mode 100644 index 8bd0b5d..0000000 --- a/src/Stott.Optimizely.RobotsHandler/Static/index-RUgWUs8j.js +++ /dev/null @@ -1,148 +0,0 @@ -(function(){const u=document.createElement("link").relList;if(u&&u.supports&&u.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))c(r);new MutationObserver(r=>{for(const f of r)if(f.type==="childList")for(const h of f.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&c(h)}).observe(document,{childList:!0,subtree:!0});function o(r){const f={};return r.integrity&&(f.integrity=r.integrity),r.referrerPolicy&&(f.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?f.credentials="include":r.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function c(r){if(r.ep)return;r.ep=!0;const f=o(r);fetch(r.href,f)}})();function di(a){return a&&a.__esModule&&Object.prototype.hasOwnProperty.call(a,"default")?a.default:a}var bo={exports:{}},ui={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ih;function N0(){if(Ih)return ui;Ih=1;var a=Symbol.for("react.transitional.element"),u=Symbol.for("react.fragment");function o(c,r,f){var h=null;if(f!==void 0&&(h=""+f),r.key!==void 0&&(h=""+r.key),"key"in r){f={};for(var p in r)p!=="key"&&(f[p]=r[p])}else f=r;return r=f.ref,{$$typeof:a,type:c,key:h,ref:r!==void 0?r:null,props:f}}return ui.Fragment=u,ui.jsx=o,ui.jsxs=o,ui}var Ph;function R0(){return Ph||(Ph=1,bo.exports=N0()),bo.exports}var m=R0(),So={exports:{}},vt={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var tm;function A0(){if(tm)return vt;tm=1;var a=Symbol.for("react.transitional.element"),u=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),c=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),f=Symbol.for("react.consumer"),h=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),E=Symbol.for("react.suspense"),v=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),C=Symbol.iterator;function D(T){return T===null||typeof T!="object"?null:(T=C&&T[C]||T["@@iterator"],typeof T=="function"?T:null)}var Q={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},M=Object.assign,X={};function B(T,U,Z){this.props=T,this.context=U,this.refs=X,this.updater=Z||Q}B.prototype.isReactComponent={},B.prototype.setState=function(T,U){if(typeof T!="object"&&typeof T!="function"&&T!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,T,U,"setState")},B.prototype.forceUpdate=function(T){this.updater.enqueueForceUpdate(this,T,"forceUpdate")};function I(){}I.prototype=B.prototype;function ut(T,U,Z){this.props=T,this.context=U,this.refs=X,this.updater=Z||Q}var J=ut.prototype=new I;J.constructor=ut,M(J,B.prototype),J.isPureReactComponent=!0;var tt=Array.isArray,k={H:null,A:null,T:null,S:null,V:null},lt=Object.prototype.hasOwnProperty;function nt(T,U,Z,V,$,yt){return Z=yt.ref,{$$typeof:a,type:T,key:U,ref:Z!==void 0?Z:null,props:yt}}function F(T,U){return nt(T.type,U,void 0,void 0,void 0,T.props)}function gt(T){return typeof T=="object"&&T!==null&&T.$$typeof===a}function K(T){var U={"=":"=0",":":"=2"};return"$"+T.replace(/[=:]/g,function(Z){return U[Z]})}var ct=/\/+/g;function Rt(T,U){return typeof T=="object"&&T!==null&&T.key!=null?K(""+T.key):U.toString(36)}function P(){}function ft(T){switch(T.status){case"fulfilled":return T.value;case"rejected":throw T.reason;default:switch(typeof T.status=="string"?T.then(P,P):(T.status="pending",T.then(function(U){T.status==="pending"&&(T.status="fulfilled",T.value=U)},function(U){T.status==="pending"&&(T.status="rejected",T.reason=U)})),T.status){case"fulfilled":return T.value;case"rejected":throw T.reason}}throw T}function G(T,U,Z,V,$){var yt=typeof T;(yt==="undefined"||yt==="boolean")&&(T=null);var ot=!1;if(T===null)ot=!0;else switch(yt){case"bigint":case"string":case"number":ot=!0;break;case"object":switch(T.$$typeof){case a:case u:ot=!0;break;case g:return ot=T._init,G(ot(T._payload),U,Z,V,$)}}if(ot)return $=$(T),ot=V===""?"."+Rt(T,0):V,tt($)?(Z="",ot!=null&&(Z=ot.replace(ct,"$&/")+"/"),G($,U,Z,"",function(fe){return fe})):$!=null&&(gt($)&&($=F($,Z+($.key==null||T&&T.key===$.key?"":(""+$.key).replace(ct,"$&/")+"/")+ot)),U.push($)),1;ot=0;var Wt=V===""?".":V+":";if(tt(T))for(var Mt=0;Mt>>1,T=x[st];if(0>>1;str(V,Y))$r(yt,V)?(x[st]=yt,x[$]=Y,st=$):(x[st]=V,x[Z]=Y,st=Z);else if($r(yt,Y))x[st]=yt,x[$]=Y,st=$;else break t}}return q}function r(x,q){var Y=x.sortIndex-q.sortIndex;return Y!==0?Y:x.id-q.id}if(a.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var f=performance;a.unstable_now=function(){return f.now()}}else{var h=Date,p=h.now();a.unstable_now=function(){return h.now()-p}}var E=[],v=[],g=1,C=null,D=3,Q=!1,M=!1,X=!1,B=!1,I=typeof setTimeout=="function"?setTimeout:null,ut=typeof clearTimeout=="function"?clearTimeout:null,J=typeof setImmediate<"u"?setImmediate:null;function tt(x){for(var q=o(v);q!==null;){if(q.callback===null)c(v);else if(q.startTime<=x)c(v),q.sortIndex=q.expirationTime,u(E,q);else break;q=o(v)}}function k(x){if(X=!1,tt(x),!M)if(o(E)!==null)M=!0,lt||(lt=!0,Rt());else{var q=o(v);q!==null&&G(k,q.startTime-x)}}var lt=!1,nt=-1,F=5,gt=-1;function K(){return B?!0:!(a.unstable_now()-gtx&&K());){var st=C.callback;if(typeof st=="function"){C.callback=null,D=C.priorityLevel;var T=st(C.expirationTime<=x);if(x=a.unstable_now(),typeof T=="function"){C.callback=T,tt(x),q=!0;break e}C===o(E)&&c(E),tt(x)}else c(E);C=o(E)}if(C!==null)q=!0;else{var U=o(v);U!==null&&G(k,U.startTime-x),q=!1}}break t}finally{C=null,D=Y,Q=!1}q=void 0}}finally{q?Rt():lt=!1}}}var Rt;if(typeof J=="function")Rt=function(){J(ct)};else if(typeof MessageChannel<"u"){var P=new MessageChannel,ft=P.port2;P.port1.onmessage=ct,Rt=function(){ft.postMessage(null)}}else Rt=function(){I(ct,0)};function G(x,q){nt=I(function(){x(a.unstable_now())},q)}a.unstable_IdlePriority=5,a.unstable_ImmediatePriority=1,a.unstable_LowPriority=4,a.unstable_NormalPriority=3,a.unstable_Profiling=null,a.unstable_UserBlockingPriority=2,a.unstable_cancelCallback=function(x){x.callback=null},a.unstable_forceFrameRate=function(x){0>x||125st?(x.sortIndex=Y,u(v,x),o(E)===null&&x===o(v)&&(X?(ut(nt),nt=-1):X=!0,G(k,Y-st))):(x.sortIndex=T,u(E,x),M||Q||(M=!0,lt||(lt=!0,Rt()))),x},a.unstable_shouldYield=K,a.unstable_wrapCallback=function(x){var q=D;return function(){var Y=D;D=q;try{return x.apply(this,arguments)}finally{D=Y}}}})(xo)),xo}var lm;function C0(){return lm||(lm=1,To.exports=O0()),To.exports}var No={exports:{}},ce={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var am;function j0(){if(am)return ce;am=1;var a=Fo();function u(E){var v="https://react.dev/errors/"+E;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(u){console.error(u)}}return a(),No.exports=j0(),No.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var um;function w0(){if(um)return si;um=1;var a=C0(),u=Fo(),o=Ym();function c(t){var e="https://react.dev/errors/"+t;if(1T||(t.current=st[T],st[T]=null,T--)}function V(t,e){T++,st[T]=t.current,t.current=e}var $=U(null),yt=U(null),ot=U(null),Wt=U(null);function Mt(t,e){switch(V(ot,e),V(yt,t),V($,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?Ah(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=Ah(e),t=Oh(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}Z($),V($,t)}function fe(){Z($),Z(yt),Z(ot)}function Ge(t){t.memoizedState!==null&&V(Wt,t);var e=$.current,n=Oh(e,t.type);e!==n&&(V(yt,t),V($,n))}function We(t){yt.current===t&&(Z($),Z(yt)),Wt.current===t&&(Z(Wt),ei._currentValue=Y)}var Xe=Object.prototype.hasOwnProperty,Ie=a.unstable_scheduleCallback,Qe=a.unstable_cancelCallback,Bt=a.unstable_shouldYield,ml=a.unstable_requestPaint,ie=a.unstable_now,as=a.unstable_getCurrentPriorityLevel,Si=a.unstable_ImmediatePriority,Ei=a.unstable_UserBlockingPriority,yl=a.unstable_NormalPriority,is=a.unstable_LowPriority,Ti=a.unstable_IdlePriority,xi=a.log,us=a.unstable_setDisableYieldValue,At=null,Zt=null;function He(t){if(typeof xi=="function"&&us(t),Zt&&typeof Zt.setStrictMode=="function")try{Zt.setStrictMode(At,t)}catch{}}var be=Math.clz32?Math.clz32:rv,cv=Math.log,ov=Math.LN2;function rv(t){return t>>>=0,t===0?32:31-(cv(t)/ov|0)|0}var Ni=256,Ri=4194304;function Qn(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Ai(t,e,n){var l=t.pendingLanes;if(l===0)return 0;var i=0,s=t.suspendedLanes,d=t.pingedLanes;t=t.warmLanes;var y=l&134217727;return y!==0?(l=y&~s,l!==0?i=Qn(l):(d&=y,d!==0?i=Qn(d):n||(n=y&~t,n!==0&&(i=Qn(n))))):(y=l&~s,y!==0?i=Qn(y):d!==0?i=Qn(d):n||(n=l&~t,n!==0&&(i=Qn(n)))),i===0?0:e!==0&&e!==i&&(e&s)===0&&(s=i&-i,n=e&-e,s>=n||s===32&&(n&4194048)!==0)?e:i}function ra(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function fv(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function cr(){var t=Ni;return Ni<<=1,(Ni&4194048)===0&&(Ni=256),t}function or(){var t=Ri;return Ri<<=1,(Ri&62914560)===0&&(Ri=4194304),t}function ss(t){for(var e=[],n=0;31>n;n++)e.push(t);return e}function fa(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function dv(t,e,n,l,i,s){var d=t.pendingLanes;t.pendingLanes=n,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=n,t.entangledLanes&=n,t.errorRecoveryDisabledLanes&=n,t.shellSuspendCounter=0;var y=t.entanglements,b=t.expirationTimes,O=t.hiddenUpdates;for(n=d&~n;0)":-1i||b[l]!==O[i]){var z=` -`+b[l].replace(" at new "," at ");return t.displayName&&z.includes("")&&(z=z.replace("",t.displayName)),z}while(1<=l&&0<=i);break}}}finally{hs=!1,Error.prepareStackTrace=n}return(n=t?t.displayName||t.name:"")?El(n):""}function gv(t){switch(t.tag){case 26:case 27:case 5:return El(t.type);case 16:return El("Lazy");case 13:return El("Suspense");case 19:return El("SuspenseList");case 0:case 15:return ms(t.type,!1);case 11:return ms(t.type.render,!1);case 1:return ms(t.type,!0);case 31:return El("Activity");default:return""}}function br(t){try{var e="";do e+=gv(t),t=t.return;while(t);return e}catch(n){return` -Error generating stack: `+n.message+` -`+n.stack}}function Oe(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function Sr(t){var e=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(e==="checkbox"||e==="radio")}function bv(t){var e=Sr(t)?"checked":"value",n=Object.getOwnPropertyDescriptor(t.constructor.prototype,e),l=""+t[e];if(!t.hasOwnProperty(e)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var i=n.get,s=n.set;return Object.defineProperty(t,e,{configurable:!0,get:function(){return i.call(this)},set:function(d){l=""+d,s.call(this,d)}}),Object.defineProperty(t,e,{enumerable:n.enumerable}),{getValue:function(){return l},setValue:function(d){l=""+d},stopTracking:function(){t._valueTracker=null,delete t[e]}}}}function ji(t){t._valueTracker||(t._valueTracker=bv(t))}function Er(t){if(!t)return!1;var e=t._valueTracker;if(!e)return!0;var n=e.getValue(),l="";return t&&(l=Sr(t)?t.checked?"true":"false":t.value),t=l,t!==n?(e.setValue(t),!0):!1}function wi(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Sv=/[\n"\\]/g;function Ce(t){return t.replace(Sv,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function ys(t,e,n,l,i,s,d,y){t.name="",d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"?t.type=d:t.removeAttribute("type"),e!=null?d==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+Oe(e)):t.value!==""+Oe(e)&&(t.value=""+Oe(e)):d!=="submit"&&d!=="reset"||t.removeAttribute("value"),e!=null?vs(t,d,Oe(e)):n!=null?vs(t,d,Oe(n)):l!=null&&t.removeAttribute("value"),i==null&&s!=null&&(t.defaultChecked=!!s),i!=null&&(t.checked=i&&typeof i!="function"&&typeof i!="symbol"),y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"?t.name=""+Oe(y):t.removeAttribute("name")}function Tr(t,e,n,l,i,s,d,y){if(s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"&&(t.type=s),e!=null||n!=null){if(!(s!=="submit"&&s!=="reset"||e!=null))return;n=n!=null?""+Oe(n):"",e=e!=null?""+Oe(e):n,y||e===t.value||(t.value=e),t.defaultValue=e}l=l??i,l=typeof l!="function"&&typeof l!="symbol"&&!!l,t.checked=y?t.checked:!!l,t.defaultChecked=!!l,d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"&&(t.name=d)}function vs(t,e,n){e==="number"&&wi(t.ownerDocument)===t||t.defaultValue===""+n||(t.defaultValue=""+n)}function Tl(t,e,n,l){if(t=t.options,e){e={};for(var i=0;i"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Es=!1;if(tn)try{var ya={};Object.defineProperty(ya,"passive",{get:function(){Es=!0}}),window.addEventListener("test",ya,ya),window.removeEventListener("test",ya,ya)}catch{Es=!1}var bn=null,Ts=null,Mi=null;function jr(){if(Mi)return Mi;var t,e=Ts,n=e.length,l,i="value"in bn?bn.value:bn.textContent,s=i.length;for(t=0;t=ga),zr=" ",Hr=!1;function Br(t,e){switch(t){case"keyup":return Jv.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Lr(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Al=!1;function $v(t,e){switch(t){case"compositionend":return Lr(e);case"keypress":return e.which!==32?null:(Hr=!0,zr);case"textInput":return t=e.data,t===zr&&Hr?null:t;default:return null}}function Wv(t,e){if(Al)return t==="compositionend"||!Os&&Br(t,e)?(t=jr(),Mi=Ts=bn=null,Al=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:n,offset:e-t};t=l}t:{for(;n;){if(n.nextSibling){n=n.nextSibling;break t}n=n.parentNode}n=void 0}n=kr(n)}}function Jr(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?Jr(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function Fr(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=wi(t.document);e instanceof t.HTMLIFrameElement;){try{var n=typeof e.contentWindow.location.href=="string"}catch{n=!1}if(n)t=e.contentWindow;else break;e=wi(t.document)}return e}function ws(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var ip=tn&&"documentMode"in document&&11>=document.documentMode,Ol=null,Ds=null,Ta=null,Ms=!1;function $r(t,e,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ms||Ol==null||Ol!==wi(l)||(l=Ol,"selectionStart"in l&&ws(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),Ta&&Ea(Ta,l)||(Ta=l,l=Tu(Ds,"onSelect"),0>=d,i-=d,nn=1<<32-be(e)+i|n<s?s:8;var d=x.T,y={};x.T=y,pc(t,!1,e,n);try{var b=i(),O=x.S;if(O!==null&&O(y,b),b!==null&&typeof b=="object"&&typeof b.then=="function"){var z=mp(b,l);Ba(t,e,z,Re(t))}else Ba(t,e,l,Re(t))}catch(L){Ba(t,e,{then:function(){},status:"rejected",reason:L},Re())}finally{q.p=s,x.T=d}}function bp(){}function yc(t,e,n,l){if(t.tag!==5)throw Error(c(476));var i=If(t).queue;Wf(t,i,e,Y,n===null?bp:function(){return Pf(t),n(l)})}function If(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:Y,baseState:Y,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:sn,lastRenderedState:Y},next:null};var n={};return e.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:sn,lastRenderedState:n},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Pf(t){var e=If(t).next.queue;Ba(t,e,{},Re())}function vc(){return se(ei)}function td(){return Kt().memoizedState}function ed(){return Kt().memoizedState}function Sp(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var n=Re();t=Tn(n);var l=xn(e,t,n);l!==null&&(Ae(l,e,n),Da(l,e,n)),e={cache:ks()},t.payload=e;return}e=e.return}}function Ep(t,e,n){var l=Re();n={lane:l,revertLane:0,action:n,hasEagerState:!1,eagerState:null,next:null},nu(t)?ld(e,n):(n=Hs(t,e,n,l),n!==null&&(Ae(n,t,l),ad(n,e,l)))}function nd(t,e,n){var l=Re();Ba(t,e,n,l)}function Ba(t,e,n,l){var i={lane:l,revertLane:0,action:n,hasEagerState:!1,eagerState:null,next:null};if(nu(t))ld(e,i);else{var s=t.alternate;if(t.lanes===0&&(s===null||s.lanes===0)&&(s=e.lastRenderedReducer,s!==null))try{var d=e.lastRenderedState,y=s(d,n);if(i.hasEagerState=!0,i.eagerState=y,Se(y,d))return qi(t,e,i,0),Lt===null&&Li(),!1}catch{}finally{}if(n=Hs(t,e,i,l),n!==null)return Ae(n,t,l),ad(n,e,l),!0}return!1}function pc(t,e,n,l){if(l={lane:2,revertLane:Fc(),action:l,hasEagerState:!1,eagerState:null,next:null},nu(t)){if(e)throw Error(c(479))}else e=Hs(t,n,l,2),e!==null&&Ae(e,t,2)}function nu(t){var e=t.alternate;return t===pt||e!==null&&e===pt}function ld(t,e){Bl=$i=!0;var n=t.pending;n===null?e.next=e:(e.next=n.next,n.next=e),t.pending=e}function ad(t,e,n){if((n&4194048)!==0){var l=e.lanes;l&=t.pendingLanes,n|=l,e.lanes=n,fr(t,n)}}var lu={readContext:se,use:Ii,useCallback:Qt,useContext:Qt,useEffect:Qt,useImperativeHandle:Qt,useLayoutEffect:Qt,useInsertionEffect:Qt,useMemo:Qt,useReducer:Qt,useRef:Qt,useState:Qt,useDebugValue:Qt,useDeferredValue:Qt,useTransition:Qt,useSyncExternalStore:Qt,useId:Qt,useHostTransitionStatus:Qt,useFormState:Qt,useActionState:Qt,useOptimistic:Qt,useMemoCache:Qt,useCacheRefresh:Qt},id={readContext:se,use:Ii,useCallback:function(t,e){return me().memoizedState=[t,e===void 0?null:e],t},useContext:se,useEffect:Xf,useImperativeHandle:function(t,e,n){n=n!=null?n.concat([t]):null,eu(4194308,4,kf.bind(null,e,t),n)},useLayoutEffect:function(t,e){return eu(4194308,4,t,e)},useInsertionEffect:function(t,e){eu(4,2,t,e)},useMemo:function(t,e){var n=me();e=e===void 0?null:e;var l=t();if(nl){He(!0);try{t()}finally{He(!1)}}return n.memoizedState=[l,e],l},useReducer:function(t,e,n){var l=me();if(n!==void 0){var i=n(e);if(nl){He(!0);try{n(e)}finally{He(!1)}}}else i=e;return l.memoizedState=l.baseState=i,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:i},l.queue=t,t=t.dispatch=Ep.bind(null,pt,t),[l.memoizedState,t]},useRef:function(t){var e=me();return t={current:t},e.memoizedState=t},useState:function(t){t=fc(t);var e=t.queue,n=nd.bind(null,pt,e);return e.dispatch=n,[t.memoizedState,n]},useDebugValue:hc,useDeferredValue:function(t,e){var n=me();return mc(n,t,e)},useTransition:function(){var t=fc(!1);return t=Wf.bind(null,pt,t.queue,!0,!1),me().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,n){var l=pt,i=me();if(jt){if(n===void 0)throw Error(c(407));n=n()}else{if(n=e(),Lt===null)throw Error(c(349));(Ot&124)!==0||Af(l,e,n)}i.memoizedState=n;var s={value:n,getSnapshot:e};return i.queue=s,Xf(Cf.bind(null,l,s,t),[t]),l.flags|=2048,ql(9,tu(),Of.bind(null,l,s,n,e),null),n},useId:function(){var t=me(),e=Lt.identifierPrefix;if(jt){var n=ln,l=nn;n=(l&~(1<<32-be(l)-1)).toString(32)+n,e="«"+e+"R"+n,n=Wi++,0it?(ee=et,et=null):ee=et.sibling;var Ct=j(R,et,A[it],H);if(Ct===null){et===null&&(et=ee);break}t&&et&&Ct.alternate===null&&e(R,et),N=s(Ct,N,it),bt===null?W=Ct:bt.sibling=Ct,bt=Ct,et=ee}if(it===A.length)return n(R,et),jt&&$n(R,it),W;if(et===null){for(;itit?(ee=et,et=null):ee=et.sibling;var Yn=j(R,et,Ct.value,H);if(Yn===null){et===null&&(et=ee);break}t&&et&&Yn.alternate===null&&e(R,et),N=s(Yn,N,it),bt===null?W=Yn:bt.sibling=Yn,bt=Yn,et=ee}if(Ct.done)return n(R,et),jt&&$n(R,it),W;if(et===null){for(;!Ct.done;it++,Ct=A.next())Ct=L(R,Ct.value,H),Ct!==null&&(N=s(Ct,N,it),bt===null?W=Ct:bt.sibling=Ct,bt=Ct);return jt&&$n(R,it),W}for(et=l(et);!Ct.done;it++,Ct=A.next())Ct=w(et,R,it,Ct.value,H),Ct!==null&&(t&&Ct.alternate!==null&&et.delete(Ct.key===null?it:Ct.key),N=s(Ct,N,it),bt===null?W=Ct:bt.sibling=Ct,bt=Ct);return t&&et.forEach(function(x0){return e(R,x0)}),jt&&$n(R,it),W}function zt(R,N,A,H){if(typeof A=="object"&&A!==null&&A.type===M&&A.key===null&&(A=A.props.children),typeof A=="object"&&A!==null){switch(A.$$typeof){case D:t:{for(var W=A.key;N!==null;){if(N.key===W){if(W=A.type,W===M){if(N.tag===7){n(R,N.sibling),H=i(N,A.props.children),H.return=R,R=H;break t}}else if(N.elementType===W||typeof W=="object"&&W!==null&&W.$$typeof===F&&sd(W)===N.type){n(R,N.sibling),H=i(N,A.props),qa(H,A),H.return=R,R=H;break t}n(R,N);break}else e(R,N);N=N.sibling}A.type===M?(H=Jn(A.props.children,R.mode,H,A.key),H.return=R,R=H):(H=Gi(A.type,A.key,A.props,null,R.mode,H),qa(H,A),H.return=R,R=H)}return d(R);case Q:t:{for(W=A.key;N!==null;){if(N.key===W)if(N.tag===4&&N.stateNode.containerInfo===A.containerInfo&&N.stateNode.implementation===A.implementation){n(R,N.sibling),H=i(N,A.children||[]),H.return=R,R=H;break t}else{n(R,N);break}else e(R,N);N=N.sibling}H=qs(A,R.mode,H),H.return=R,R=H}return d(R);case F:return W=A._init,A=W(A._payload),zt(R,N,A,H)}if(G(A))return rt(R,N,A,H);if(Rt(A)){if(W=Rt(A),typeof W!="function")throw Error(c(150));return A=W.call(A),at(R,N,A,H)}if(typeof A.then=="function")return zt(R,N,au(A),H);if(A.$$typeof===J)return zt(R,N,Zi(R,A),H);iu(R,A)}return typeof A=="string"&&A!==""||typeof A=="number"||typeof A=="bigint"?(A=""+A,N!==null&&N.tag===6?(n(R,N.sibling),H=i(N,A),H.return=R,R=H):(n(R,N),H=Ls(A,R.mode,H),H.return=R,R=H),d(R)):n(R,N)}return function(R,N,A,H){try{La=0;var W=zt(R,N,A,H);return Yl=null,W}catch(et){if(et===ja||et===Ki)throw et;var bt=Ee(29,et,null,R.mode);return bt.lanes=H,bt.return=R,bt}finally{}}}var Gl=cd(!0),od=cd(!1),_e=U(null),Ze=null;function Rn(t){var e=t.alternate;V($t,$t.current&1),V(_e,t),Ze===null&&(e===null||Hl.current!==null||e.memoizedState!==null)&&(Ze=t)}function rd(t){if(t.tag===22){if(V($t,$t.current),V(_e,t),Ze===null){var e=t.alternate;e!==null&&e.memoizedState!==null&&(Ze=t)}}else An()}function An(){V($t,$t.current),V(_e,_e.current)}function cn(t){Z(_e),Ze===t&&(Ze=null),Z($t)}var $t=U(0);function uu(t){for(var e=t;e!==null;){if(e.tag===13){var n=e.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||so(n)))return e}else if(e.tag===19&&e.memoizedProps.revealOrder!==void 0){if((e.flags&128)!==0)return e}else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break;for(;e.sibling===null;){if(e.return===null||e.return===t)return null;e=e.return}e.sibling.return=e.return,e=e.sibling}return null}function gc(t,e,n,l){e=t.memoizedState,n=n(l,e),n=n==null?e:g({},e,n),t.memoizedState=n,t.lanes===0&&(t.updateQueue.baseState=n)}var bc={enqueueSetState:function(t,e,n){t=t._reactInternals;var l=Re(),i=Tn(l);i.payload=e,n!=null&&(i.callback=n),e=xn(t,i,l),e!==null&&(Ae(e,t,l),Da(e,t,l))},enqueueReplaceState:function(t,e,n){t=t._reactInternals;var l=Re(),i=Tn(l);i.tag=1,i.payload=e,n!=null&&(i.callback=n),e=xn(t,i,l),e!==null&&(Ae(e,t,l),Da(e,t,l))},enqueueForceUpdate:function(t,e){t=t._reactInternals;var n=Re(),l=Tn(n);l.tag=2,e!=null&&(l.callback=e),e=xn(t,l,n),e!==null&&(Ae(e,t,n),Da(e,t,n))}};function fd(t,e,n,l,i,s,d){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(l,s,d):e.prototype&&e.prototype.isPureReactComponent?!Ea(n,l)||!Ea(i,s):!0}function dd(t,e,n,l){t=e.state,typeof e.componentWillReceiveProps=="function"&&e.componentWillReceiveProps(n,l),typeof e.UNSAFE_componentWillReceiveProps=="function"&&e.UNSAFE_componentWillReceiveProps(n,l),e.state!==t&&bc.enqueueReplaceState(e,e.state,null)}function ll(t,e){var n=e;if("ref"in e){n={};for(var l in e)l!=="ref"&&(n[l]=e[l])}if(t=t.defaultProps){n===e&&(n=g({},n));for(var i in t)n[i]===void 0&&(n[i]=t[i])}return n}var su=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var e=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(e))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)};function hd(t){su(t)}function md(t){console.error(t)}function yd(t){su(t)}function cu(t,e){try{var n=t.onUncaughtError;n(e.value,{componentStack:e.stack})}catch(l){setTimeout(function(){throw l})}}function vd(t,e,n){try{var l=t.onCaughtError;l(n.value,{componentStack:n.stack,errorBoundary:e.tag===1?e.stateNode:null})}catch(i){setTimeout(function(){throw i})}}function Sc(t,e,n){return n=Tn(n),n.tag=3,n.payload={element:null},n.callback=function(){cu(t,e)},n}function pd(t){return t=Tn(t),t.tag=3,t}function gd(t,e,n,l){var i=n.type.getDerivedStateFromError;if(typeof i=="function"){var s=l.value;t.payload=function(){return i(s)},t.callback=function(){vd(e,n,l)}}var d=n.stateNode;d!==null&&typeof d.componentDidCatch=="function"&&(t.callback=function(){vd(e,n,l),typeof i!="function"&&(Mn===null?Mn=new Set([this]):Mn.add(this));var y=l.stack;this.componentDidCatch(l.value,{componentStack:y!==null?y:""})})}function xp(t,e,n,l,i){if(n.flags|=32768,l!==null&&typeof l=="object"&&typeof l.then=="function"){if(e=n.alternate,e!==null&&Aa(e,n,i,!0),n=_e.current,n!==null){switch(n.tag){case 13:return Ze===null?Vc():n.alternate===null&&Xt===0&&(Xt=3),n.flags&=-257,n.flags|=65536,n.lanes=i,l===Fs?n.flags|=16384:(e=n.updateQueue,e===null?n.updateQueue=new Set([l]):e.add(l),kc(t,l,i)),!1;case 22:return n.flags|=65536,l===Fs?n.flags|=16384:(e=n.updateQueue,e===null?(e={transitions:null,markerInstances:null,retryQueue:new Set([l])},n.updateQueue=e):(n=e.retryQueue,n===null?e.retryQueue=new Set([l]):n.add(l)),kc(t,l,i)),!1}throw Error(c(435,n.tag))}return kc(t,l,i),Vc(),!1}if(jt)return e=_e.current,e!==null?((e.flags&65536)===0&&(e.flags|=256),e.flags|=65536,e.lanes=i,l!==Xs&&(t=Error(c(422),{cause:l}),Ra(je(t,n)))):(l!==Xs&&(e=Error(c(423),{cause:l}),Ra(je(e,n))),t=t.current.alternate,t.flags|=65536,i&=-i,t.lanes|=i,l=je(l,n),i=Sc(t.stateNode,l,i),Is(t,i),Xt!==4&&(Xt=2)),!1;var s=Error(c(520),{cause:l});if(s=je(s,n),ka===null?ka=[s]:ka.push(s),Xt!==4&&(Xt=2),e===null)return!0;l=je(l,n),n=e;do{switch(n.tag){case 3:return n.flags|=65536,t=i&-i,n.lanes|=t,t=Sc(n.stateNode,l,t),Is(n,t),!1;case 1:if(e=n.type,s=n.stateNode,(n.flags&128)===0&&(typeof e.getDerivedStateFromError=="function"||s!==null&&typeof s.componentDidCatch=="function"&&(Mn===null||!Mn.has(s))))return n.flags|=65536,i&=-i,n.lanes|=i,i=pd(i),gd(i,t,n,l),Is(n,i),!1}n=n.return}while(n!==null);return!1}var bd=Error(c(461)),Pt=!1;function ne(t,e,n,l){e.child=t===null?od(e,null,n,l):Gl(e,t.child,n,l)}function Sd(t,e,n,l,i){n=n.render;var s=e.ref;if("ref"in l){var d={};for(var y in l)y!=="ref"&&(d[y]=l[y])}else d=l;return tl(e),l=lc(t,e,n,d,s,i),y=ac(),t!==null&&!Pt?(ic(t,e,i),on(t,e,i)):(jt&&y&&Ys(e),e.flags|=1,ne(t,e,l,i),e.child)}function Ed(t,e,n,l,i){if(t===null){var s=n.type;return typeof s=="function"&&!Bs(s)&&s.defaultProps===void 0&&n.compare===null?(e.tag=15,e.type=s,Td(t,e,s,l,i)):(t=Gi(n.type,null,l,e,e.mode,i),t.ref=e.ref,t.return=e,e.child=t)}if(s=t.child,!Cc(t,i)){var d=s.memoizedProps;if(n=n.compare,n=n!==null?n:Ea,n(d,l)&&t.ref===e.ref)return on(t,e,i)}return e.flags|=1,t=en(s,l),t.ref=e.ref,t.return=e,e.child=t}function Td(t,e,n,l,i){if(t!==null){var s=t.memoizedProps;if(Ea(s,l)&&t.ref===e.ref)if(Pt=!1,e.pendingProps=l=s,Cc(t,i))(t.flags&131072)!==0&&(Pt=!0);else return e.lanes=t.lanes,on(t,e,i)}return Ec(t,e,n,l,i)}function xd(t,e,n){var l=e.pendingProps,i=l.children,s=t!==null?t.memoizedState:null;if(l.mode==="hidden"){if((e.flags&128)!==0){if(l=s!==null?s.baseLanes|n:n,t!==null){for(i=e.child=t.child,s=0;i!==null;)s=s|i.lanes|i.childLanes,i=i.sibling;e.childLanes=s&~l}else e.childLanes=0,e.child=null;return Nd(t,e,l,n)}if((n&536870912)!==0)e.memoizedState={baseLanes:0,cachePool:null},t!==null&&ki(e,s!==null?s.cachePool:null),s!==null?Tf(e,s):tc(),rd(e);else return e.lanes=e.childLanes=536870912,Nd(t,e,s!==null?s.baseLanes|n:n,n)}else s!==null?(ki(e,s.cachePool),Tf(e,s),An(),e.memoizedState=null):(t!==null&&ki(e,null),tc(),An());return ne(t,e,i,n),e.child}function Nd(t,e,n,l){var i=Js();return i=i===null?null:{parent:Ft._currentValue,pool:i},e.memoizedState={baseLanes:n,cachePool:i},t!==null&&ki(e,null),tc(),rd(e),t!==null&&Aa(t,e,l,!0),null}function ou(t,e){var n=e.ref;if(n===null)t!==null&&t.ref!==null&&(e.flags|=4194816);else{if(typeof n!="function"&&typeof n!="object")throw Error(c(284));(t===null||t.ref!==n)&&(e.flags|=4194816)}}function Ec(t,e,n,l,i){return tl(e),n=lc(t,e,n,l,void 0,i),l=ac(),t!==null&&!Pt?(ic(t,e,i),on(t,e,i)):(jt&&l&&Ys(e),e.flags|=1,ne(t,e,n,i),e.child)}function Rd(t,e,n,l,i,s){return tl(e),e.updateQueue=null,n=Nf(e,l,n,i),xf(t),l=ac(),t!==null&&!Pt?(ic(t,e,s),on(t,e,s)):(jt&&l&&Ys(e),e.flags|=1,ne(t,e,n,s),e.child)}function Ad(t,e,n,l,i){if(tl(e),e.stateNode===null){var s=Dl,d=n.contextType;typeof d=="object"&&d!==null&&(s=se(d)),s=new n(l,s),e.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,s.updater=bc,e.stateNode=s,s._reactInternals=e,s=e.stateNode,s.props=l,s.state=e.memoizedState,s.refs={},$s(e),d=n.contextType,s.context=typeof d=="object"&&d!==null?se(d):Dl,s.state=e.memoizedState,d=n.getDerivedStateFromProps,typeof d=="function"&&(gc(e,n,d,l),s.state=e.memoizedState),typeof n.getDerivedStateFromProps=="function"||typeof s.getSnapshotBeforeUpdate=="function"||typeof s.UNSAFE_componentWillMount!="function"&&typeof s.componentWillMount!="function"||(d=s.state,typeof s.componentWillMount=="function"&&s.componentWillMount(),typeof s.UNSAFE_componentWillMount=="function"&&s.UNSAFE_componentWillMount(),d!==s.state&&bc.enqueueReplaceState(s,s.state,null),_a(e,l,s,i),Ma(),s.state=e.memoizedState),typeof s.componentDidMount=="function"&&(e.flags|=4194308),l=!0}else if(t===null){s=e.stateNode;var y=e.memoizedProps,b=ll(n,y);s.props=b;var O=s.context,z=n.contextType;d=Dl,typeof z=="object"&&z!==null&&(d=se(z));var L=n.getDerivedStateFromProps;z=typeof L=="function"||typeof s.getSnapshotBeforeUpdate=="function",y=e.pendingProps!==y,z||typeof s.UNSAFE_componentWillReceiveProps!="function"&&typeof s.componentWillReceiveProps!="function"||(y||O!==d)&&dd(e,s,l,d),En=!1;var j=e.memoizedState;s.state=j,_a(e,l,s,i),Ma(),O=e.memoizedState,y||j!==O||En?(typeof L=="function"&&(gc(e,n,L,l),O=e.memoizedState),(b=En||fd(e,n,b,l,j,O,d))?(z||typeof s.UNSAFE_componentWillMount!="function"&&typeof s.componentWillMount!="function"||(typeof s.componentWillMount=="function"&&s.componentWillMount(),typeof s.UNSAFE_componentWillMount=="function"&&s.UNSAFE_componentWillMount()),typeof s.componentDidMount=="function"&&(e.flags|=4194308)):(typeof s.componentDidMount=="function"&&(e.flags|=4194308),e.memoizedProps=l,e.memoizedState=O),s.props=l,s.state=O,s.context=d,l=b):(typeof s.componentDidMount=="function"&&(e.flags|=4194308),l=!1)}else{s=e.stateNode,Ws(t,e),d=e.memoizedProps,z=ll(n,d),s.props=z,L=e.pendingProps,j=s.context,O=n.contextType,b=Dl,typeof O=="object"&&O!==null&&(b=se(O)),y=n.getDerivedStateFromProps,(O=typeof y=="function"||typeof s.getSnapshotBeforeUpdate=="function")||typeof s.UNSAFE_componentWillReceiveProps!="function"&&typeof s.componentWillReceiveProps!="function"||(d!==L||j!==b)&&dd(e,s,l,b),En=!1,j=e.memoizedState,s.state=j,_a(e,l,s,i),Ma();var w=e.memoizedState;d!==L||j!==w||En||t!==null&&t.dependencies!==null&&Vi(t.dependencies)?(typeof y=="function"&&(gc(e,n,y,l),w=e.memoizedState),(z=En||fd(e,n,z,l,j,w,b)||t!==null&&t.dependencies!==null&&Vi(t.dependencies))?(O||typeof s.UNSAFE_componentWillUpdate!="function"&&typeof s.componentWillUpdate!="function"||(typeof s.componentWillUpdate=="function"&&s.componentWillUpdate(l,w,b),typeof s.UNSAFE_componentWillUpdate=="function"&&s.UNSAFE_componentWillUpdate(l,w,b)),typeof s.componentDidUpdate=="function"&&(e.flags|=4),typeof s.getSnapshotBeforeUpdate=="function"&&(e.flags|=1024)):(typeof s.componentDidUpdate!="function"||d===t.memoizedProps&&j===t.memoizedState||(e.flags|=4),typeof s.getSnapshotBeforeUpdate!="function"||d===t.memoizedProps&&j===t.memoizedState||(e.flags|=1024),e.memoizedProps=l,e.memoizedState=w),s.props=l,s.state=w,s.context=b,l=z):(typeof s.componentDidUpdate!="function"||d===t.memoizedProps&&j===t.memoizedState||(e.flags|=4),typeof s.getSnapshotBeforeUpdate!="function"||d===t.memoizedProps&&j===t.memoizedState||(e.flags|=1024),l=!1)}return s=l,ou(t,e),l=(e.flags&128)!==0,s||l?(s=e.stateNode,n=l&&typeof n.getDerivedStateFromError!="function"?null:s.render(),e.flags|=1,t!==null&&l?(e.child=Gl(e,t.child,null,i),e.child=Gl(e,null,n,i)):ne(t,e,n,i),e.memoizedState=s.state,t=e.child):t=on(t,e,i),t}function Od(t,e,n,l){return Na(),e.flags|=256,ne(t,e,n,l),e.child}var Tc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function xc(t){return{baseLanes:t,cachePool:mf()}}function Nc(t,e,n){return t=t!==null?t.childLanes&~n:0,e&&(t|=Ue),t}function Cd(t,e,n){var l=e.pendingProps,i=!1,s=(e.flags&128)!==0,d;if((d=s)||(d=t!==null&&t.memoizedState===null?!1:($t.current&2)!==0),d&&(i=!0,e.flags&=-129),d=(e.flags&32)!==0,e.flags&=-33,t===null){if(jt){if(i?Rn(e):An(),jt){var y=Gt,b;if(b=y){t:{for(b=y,y=Ve;b.nodeType!==8;){if(!y){y=null;break t}if(b=qe(b.nextSibling),b===null){y=null;break t}}y=b}y!==null?(e.memoizedState={dehydrated:y,treeContext:Fn!==null?{id:nn,overflow:ln}:null,retryLane:536870912,hydrationErrors:null},b=Ee(18,null,null,0),b.stateNode=y,b.return=e,e.child=b,re=e,Gt=null,b=!0):b=!1}b||In(e)}if(y=e.memoizedState,y!==null&&(y=y.dehydrated,y!==null))return so(y)?e.lanes=32:e.lanes=536870912,null;cn(e)}return y=l.children,l=l.fallback,i?(An(),i=e.mode,y=ru({mode:"hidden",children:y},i),l=Jn(l,i,n,null),y.return=e,l.return=e,y.sibling=l,e.child=y,i=e.child,i.memoizedState=xc(n),i.childLanes=Nc(t,d,n),e.memoizedState=Tc,l):(Rn(e),Rc(e,y))}if(b=t.memoizedState,b!==null&&(y=b.dehydrated,y!==null)){if(s)e.flags&256?(Rn(e),e.flags&=-257,e=Ac(t,e,n)):e.memoizedState!==null?(An(),e.child=t.child,e.flags|=128,e=null):(An(),i=l.fallback,y=e.mode,l=ru({mode:"visible",children:l.children},y),i=Jn(i,y,n,null),i.flags|=2,l.return=e,i.return=e,l.sibling=i,e.child=l,Gl(e,t.child,null,n),l=e.child,l.memoizedState=xc(n),l.childLanes=Nc(t,d,n),e.memoizedState=Tc,e=i);else if(Rn(e),so(y)){if(d=y.nextSibling&&y.nextSibling.dataset,d)var O=d.dgst;d=O,l=Error(c(419)),l.stack="",l.digest=d,Ra({value:l,source:null,stack:null}),e=Ac(t,e,n)}else if(Pt||Aa(t,e,n,!1),d=(n&t.childLanes)!==0,Pt||d){if(d=Lt,d!==null&&(l=n&-n,l=(l&42)!==0?1:cs(l),l=(l&(d.suspendedLanes|n))!==0?0:l,l!==0&&l!==b.retryLane))throw b.retryLane=l,wl(t,l),Ae(d,t,l),bd;y.data==="$?"||Vc(),e=Ac(t,e,n)}else y.data==="$?"?(e.flags|=192,e.child=t.child,e=null):(t=b.treeContext,Gt=qe(y.nextSibling),re=e,jt=!0,Wn=null,Ve=!1,t!==null&&(De[Me++]=nn,De[Me++]=ln,De[Me++]=Fn,nn=t.id,ln=t.overflow,Fn=e),e=Rc(e,l.children),e.flags|=4096);return e}return i?(An(),i=l.fallback,y=e.mode,b=t.child,O=b.sibling,l=en(b,{mode:"hidden",children:l.children}),l.subtreeFlags=b.subtreeFlags&65011712,O!==null?i=en(O,i):(i=Jn(i,y,n,null),i.flags|=2),i.return=e,l.return=e,l.sibling=i,e.child=l,l=i,i=e.child,y=t.child.memoizedState,y===null?y=xc(n):(b=y.cachePool,b!==null?(O=Ft._currentValue,b=b.parent!==O?{parent:O,pool:O}:b):b=mf(),y={baseLanes:y.baseLanes|n,cachePool:b}),i.memoizedState=y,i.childLanes=Nc(t,d,n),e.memoizedState=Tc,l):(Rn(e),n=t.child,t=n.sibling,n=en(n,{mode:"visible",children:l.children}),n.return=e,n.sibling=null,t!==null&&(d=e.deletions,d===null?(e.deletions=[t],e.flags|=16):d.push(t)),e.child=n,e.memoizedState=null,n)}function Rc(t,e){return e=ru({mode:"visible",children:e},t.mode),e.return=t,t.child=e}function ru(t,e){return t=Ee(22,t,null,e),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function Ac(t,e,n){return Gl(e,t.child,null,n),t=Rc(e,e.pendingProps.children),t.flags|=2,e.memoizedState=null,t}function jd(t,e,n){t.lanes|=e;var l=t.alternate;l!==null&&(l.lanes|=e),Vs(t.return,e,n)}function Oc(t,e,n,l,i){var s=t.memoizedState;s===null?t.memoizedState={isBackwards:e,rendering:null,renderingStartTime:0,last:l,tail:n,tailMode:i}:(s.isBackwards=e,s.rendering=null,s.renderingStartTime=0,s.last=l,s.tail=n,s.tailMode=i)}function wd(t,e,n){var l=e.pendingProps,i=l.revealOrder,s=l.tail;if(ne(t,e,l.children,n),l=$t.current,(l&2)!==0)l=l&1|2,e.flags|=128;else{if(t!==null&&(t.flags&128)!==0)t:for(t=e.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&jd(t,n,e);else if(t.tag===19)jd(t,n,e);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break t;for(;t.sibling===null;){if(t.return===null||t.return===e)break t;t=t.return}t.sibling.return=t.return,t=t.sibling}l&=1}switch(V($t,l),i){case"forwards":for(n=e.child,i=null;n!==null;)t=n.alternate,t!==null&&uu(t)===null&&(i=n),n=n.sibling;n=i,n===null?(i=e.child,e.child=null):(i=n.sibling,n.sibling=null),Oc(e,!1,i,n,s);break;case"backwards":for(n=null,i=e.child,e.child=null;i!==null;){if(t=i.alternate,t!==null&&uu(t)===null){e.child=i;break}t=i.sibling,i.sibling=n,n=i,i=t}Oc(e,!0,n,null,s);break;case"together":Oc(e,!1,null,null,void 0);break;default:e.memoizedState=null}return e.child}function on(t,e,n){if(t!==null&&(e.dependencies=t.dependencies),Dn|=e.lanes,(n&e.childLanes)===0)if(t!==null){if(Aa(t,e,n,!1),(n&e.childLanes)===0)return null}else return null;if(t!==null&&e.child!==t.child)throw Error(c(153));if(e.child!==null){for(t=e.child,n=en(t,t.pendingProps),e.child=n,n.return=e;t.sibling!==null;)t=t.sibling,n=n.sibling=en(t,t.pendingProps),n.return=e;n.sibling=null}return e.child}function Cc(t,e){return(t.lanes&e)!==0?!0:(t=t.dependencies,!!(t!==null&&Vi(t)))}function Np(t,e,n){switch(e.tag){case 3:Mt(e,e.stateNode.containerInfo),Sn(e,Ft,t.memoizedState.cache),Na();break;case 27:case 5:Ge(e);break;case 4:Mt(e,e.stateNode.containerInfo);break;case 10:Sn(e,e.type,e.memoizedProps.value);break;case 13:var l=e.memoizedState;if(l!==null)return l.dehydrated!==null?(Rn(e),e.flags|=128,null):(n&e.child.childLanes)!==0?Cd(t,e,n):(Rn(e),t=on(t,e,n),t!==null?t.sibling:null);Rn(e);break;case 19:var i=(t.flags&128)!==0;if(l=(n&e.childLanes)!==0,l||(Aa(t,e,n,!1),l=(n&e.childLanes)!==0),i){if(l)return wd(t,e,n);e.flags|=128}if(i=e.memoizedState,i!==null&&(i.rendering=null,i.tail=null,i.lastEffect=null),V($t,$t.current),l)break;return null;case 22:case 23:return e.lanes=0,xd(t,e,n);case 24:Sn(e,Ft,t.memoizedState.cache)}return on(t,e,n)}function Dd(t,e,n){if(t!==null)if(t.memoizedProps!==e.pendingProps)Pt=!0;else{if(!Cc(t,n)&&(e.flags&128)===0)return Pt=!1,Np(t,e,n);Pt=(t.flags&131072)!==0}else Pt=!1,jt&&(e.flags&1048576)!==0&&sf(e,Qi,e.index);switch(e.lanes=0,e.tag){case 16:t:{t=e.pendingProps;var l=e.elementType,i=l._init;if(l=i(l._payload),e.type=l,typeof l=="function")Bs(l)?(t=ll(l,t),e.tag=1,e=Ad(null,e,l,t,n)):(e.tag=0,e=Ec(null,e,l,t,n));else{if(l!=null){if(i=l.$$typeof,i===tt){e.tag=11,e=Sd(null,e,l,t,n);break t}else if(i===nt){e.tag=14,e=Ed(null,e,l,t,n);break t}}throw e=ft(l)||l,Error(c(306,e,""))}}return e;case 0:return Ec(t,e,e.type,e.pendingProps,n);case 1:return l=e.type,i=ll(l,e.pendingProps),Ad(t,e,l,i,n);case 3:t:{if(Mt(e,e.stateNode.containerInfo),t===null)throw Error(c(387));l=e.pendingProps;var s=e.memoizedState;i=s.element,Ws(t,e),_a(e,l,null,n);var d=e.memoizedState;if(l=d.cache,Sn(e,Ft,l),l!==s.cache&&Zs(e,[Ft],n,!0),Ma(),l=d.element,s.isDehydrated)if(s={element:l,isDehydrated:!1,cache:d.cache},e.updateQueue.baseState=s,e.memoizedState=s,e.flags&256){e=Od(t,e,l,n);break t}else if(l!==i){i=je(Error(c(424)),e),Ra(i),e=Od(t,e,l,n);break t}else{switch(t=e.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(Gt=qe(t.firstChild),re=e,jt=!0,Wn=null,Ve=!0,n=od(e,null,l,n),e.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling}else{if(Na(),l===i){e=on(t,e,n);break t}ne(t,e,l,n)}e=e.child}return e;case 26:return ou(t,e),t===null?(n=zh(e.type,null,e.pendingProps,null))?e.memoizedState=n:jt||(n=e.type,t=e.pendingProps,l=Nu(ot.current).createElement(n),l[ue]=e,l[de]=t,ae(l,n,t),It(l),e.stateNode=l):e.memoizedState=zh(e.type,t.memoizedProps,e.pendingProps,t.memoizedState),null;case 27:return Ge(e),t===null&&jt&&(l=e.stateNode=Mh(e.type,e.pendingProps,ot.current),re=e,Ve=!0,i=Gt,zn(e.type)?(co=i,Gt=qe(l.firstChild)):Gt=i),ne(t,e,e.pendingProps.children,n),ou(t,e),t===null&&(e.flags|=4194304),e.child;case 5:return t===null&&jt&&((i=l=Gt)&&(l=Ip(l,e.type,e.pendingProps,Ve),l!==null?(e.stateNode=l,re=e,Gt=qe(l.firstChild),Ve=!1,i=!0):i=!1),i||In(e)),Ge(e),i=e.type,s=e.pendingProps,d=t!==null?t.memoizedProps:null,l=s.children,ao(i,s)?l=null:d!==null&&ao(i,d)&&(e.flags|=32),e.memoizedState!==null&&(i=lc(t,e,vp,null,null,n),ei._currentValue=i),ou(t,e),ne(t,e,l,n),e.child;case 6:return t===null&&jt&&((t=n=Gt)&&(n=Pp(n,e.pendingProps,Ve),n!==null?(e.stateNode=n,re=e,Gt=null,t=!0):t=!1),t||In(e)),null;case 13:return Cd(t,e,n);case 4:return Mt(e,e.stateNode.containerInfo),l=e.pendingProps,t===null?e.child=Gl(e,null,l,n):ne(t,e,l,n),e.child;case 11:return Sd(t,e,e.type,e.pendingProps,n);case 7:return ne(t,e,e.pendingProps,n),e.child;case 8:return ne(t,e,e.pendingProps.children,n),e.child;case 12:return ne(t,e,e.pendingProps.children,n),e.child;case 10:return l=e.pendingProps,Sn(e,e.type,l.value),ne(t,e,l.children,n),e.child;case 9:return i=e.type._context,l=e.pendingProps.children,tl(e),i=se(i),l=l(i),e.flags|=1,ne(t,e,l,n),e.child;case 14:return Ed(t,e,e.type,e.pendingProps,n);case 15:return Td(t,e,e.type,e.pendingProps,n);case 19:return wd(t,e,n);case 31:return l=e.pendingProps,n=e.mode,l={mode:l.mode,children:l.children},t===null?(n=ru(l,n),n.ref=e.ref,e.child=n,n.return=e,e=n):(n=en(t.child,l),n.ref=e.ref,e.child=n,n.return=e,e=n),e;case 22:return xd(t,e,n);case 24:return tl(e),l=se(Ft),t===null?(i=Js(),i===null&&(i=Lt,s=ks(),i.pooledCache=s,s.refCount++,s!==null&&(i.pooledCacheLanes|=n),i=s),e.memoizedState={parent:l,cache:i},$s(e),Sn(e,Ft,i)):((t.lanes&n)!==0&&(Ws(t,e),_a(e,null,null,n),Ma()),i=t.memoizedState,s=e.memoizedState,i.parent!==l?(i={parent:l,cache:l},e.memoizedState=i,e.lanes===0&&(e.memoizedState=e.updateQueue.baseState=i),Sn(e,Ft,l)):(l=s.cache,Sn(e,Ft,l),l!==i.cache&&Zs(e,[Ft],n,!0))),ne(t,e,e.pendingProps.children,n),e.child;case 29:throw e.pendingProps}throw Error(c(156,e.tag))}function rn(t){t.flags|=4}function Md(t,e){if(e.type!=="stylesheet"||(e.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!Yh(e)){if(e=_e.current,e!==null&&((Ot&4194048)===Ot?Ze!==null:(Ot&62914560)!==Ot&&(Ot&536870912)===0||e!==Ze))throw wa=Fs,yf;t.flags|=8192}}function fu(t,e){e!==null&&(t.flags|=4),t.flags&16384&&(e=t.tag!==22?or():536870912,t.lanes|=e,Zl|=e)}function Ya(t,e){if(!jt)switch(t.tailMode){case"hidden":e=t.tail;for(var n=null;e!==null;)e.alternate!==null&&(n=e),e=e.sibling;n===null?t.tail=null:n.sibling=null;break;case"collapsed":n=t.tail;for(var l=null;n!==null;)n.alternate!==null&&(l=n),n=n.sibling;l===null?e||t.tail===null?t.tail=null:t.tail.sibling=null:l.sibling=null}}function Yt(t){var e=t.alternate!==null&&t.alternate.child===t.child,n=0,l=0;if(e)for(var i=t.child;i!==null;)n|=i.lanes|i.childLanes,l|=i.subtreeFlags&65011712,l|=i.flags&65011712,i.return=t,i=i.sibling;else for(i=t.child;i!==null;)n|=i.lanes|i.childLanes,l|=i.subtreeFlags,l|=i.flags,i.return=t,i=i.sibling;return t.subtreeFlags|=l,t.childLanes=n,e}function Rp(t,e,n){var l=e.pendingProps;switch(Gs(e),e.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Yt(e),null;case 1:return Yt(e),null;case 3:return n=e.stateNode,l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),un(Ft),fe(),n.pendingContext&&(n.context=n.pendingContext,n.pendingContext=null),(t===null||t.child===null)&&(xa(e)?rn(e):t===null||t.memoizedState.isDehydrated&&(e.flags&256)===0||(e.flags|=1024,rf())),Yt(e),null;case 26:return n=e.memoizedState,t===null?(rn(e),n!==null?(Yt(e),Md(e,n)):(Yt(e),e.flags&=-16777217)):n?n!==t.memoizedState?(rn(e),Yt(e),Md(e,n)):(Yt(e),e.flags&=-16777217):(t.memoizedProps!==l&&rn(e),Yt(e),e.flags&=-16777217),null;case 27:We(e),n=ot.current;var i=e.type;if(t!==null&&e.stateNode!=null)t.memoizedProps!==l&&rn(e);else{if(!l){if(e.stateNode===null)throw Error(c(166));return Yt(e),null}t=$.current,xa(e)?cf(e):(t=Mh(i,l,n),e.stateNode=t,rn(e))}return Yt(e),null;case 5:if(We(e),n=e.type,t!==null&&e.stateNode!=null)t.memoizedProps!==l&&rn(e);else{if(!l){if(e.stateNode===null)throw Error(c(166));return Yt(e),null}if(t=$.current,xa(e))cf(e);else{switch(i=Nu(ot.current),t){case 1:t=i.createElementNS("http://www.w3.org/2000/svg",n);break;case 2:t=i.createElementNS("http://www.w3.org/1998/Math/MathML",n);break;default:switch(n){case"svg":t=i.createElementNS("http://www.w3.org/2000/svg",n);break;case"math":t=i.createElementNS("http://www.w3.org/1998/Math/MathML",n);break;case"script":t=i.createElement("div"),t.innerHTML="