diff --git a/.github/New BackOffice - README.md b/.github/New BackOffice - README.md new file mode 100644 index 0000000000..3d04ef2e36 --- /dev/null +++ b/.github/New BackOffice - README.md @@ -0,0 +1,18 @@ +# New Backoffice + +> **Warning**: +> This is an early WIP, and is set not to be packable since we don't want to release this yet. There will be breaking changes in these projects + +This solution folder contains the projects for the new BackOffice. If you're looking to fix or improve the existing CMS, this is not the place to do it, although we do very much appreciate your efforts. + +### Project structure + +Since the new backoffice API is still very much a work in progress we've created new projects for the new backoffice API: + +* Umbrao.Cms.ManagementApi - The "presentation layer" for the management API +* "New" versions of existing projects, should be merged with the existing projects when the new API is released: + * Umbraco.New.Cms.Core + * Umbraco.New.Cms.Infrastructure + * Umbraco.New.Cms.Web.Common + +This also means that we have to use "InternalsVisibleTo" for the new projects since these should be able to access the internal classes since they will when they get merged. diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs new file mode 100644 index 0000000000..94029edad4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Install; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[BackOfficeRoute("api/v{version:apiVersion}/install")] +[RequireRuntimeLevel(RuntimeLevel.Install)] +public class NewInstallController : Controller +{ + private readonly IUmbracoMapper _mapper; + private readonly IInstallSettingsFactory _installSettingsFactory; + private readonly IInstallService _installService; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly InstallHelper _installHelper; + private readonly DatabaseBuilder _databaseBuilder; + + public NewInstallController( + IUmbracoMapper mapper, + IInstallSettingsFactory installSettingsFactory, + IInstallService installService, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + InstallHelper installHelper, + DatabaseBuilder databaseBuilder) + { + _mapper = mapper; + _installSettingsFactory = installSettingsFactory; + _installService = installService; + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; + _installHelper = installHelper; + _databaseBuilder = databaseBuilder; + } + + [HttpGet("settings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(typeof(InstallSettingsViewModel), StatusCodes.Status200OK)] + public async Task> Settings() + { + // Register that the install has started + await _installHelper.SetInstallStatusAsync(false, string.Empty); + + InstallSettingsModel installSettings = _installSettingsFactory.GetInstallSettings(); + InstallSettingsViewModel viewModel = _mapper.Map(installSettings)!; + + return viewModel; + } + + [HttpPost("setup")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Setup(InstallViewModel installData) + { + InstallData data = _mapper.Map(installData)!; + await _installService.Install(data); + + var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); + return Created(backOfficePath, null); + } + + [HttpPost("validateDatabase")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ValidateDatabase(DatabaseInstallViewModel viewModel) + { + // TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async + // We want these to be async for future use (Ideally we'll have more async things), + // But we need to figure out how we want to handle it in the meantime? use Task.FromResult or? + DatabaseModel databaseModel = _mapper.Map(viewModel)!; + + var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true); + + if (success) + { + return Ok(); + } + + var invalidModelProblem = new ProblemDetails + { + Title = "Invalid database configuration", + Detail = "The provided database configuration is invalid", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return BadRequest(invalidModelProblem); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs new file mode 100644 index 0000000000..29164adfb2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers; + +// TODO: This needs to be an authorized controller. +[ApiController] +[ApiVersion("1.0")] +[RequireRuntimeLevel(RuntimeLevel.Upgrade)] +[BackOfficeRoute("api/v{version:apiVersion}/upgrade")] +public class UpgradeController : Controller +{ + private readonly IUpgradeSettingsFactory _upgradeSettingsFactory; + private readonly IUpgradeService _upgradeService; + private readonly IUmbracoMapper _mapper; + + public UpgradeController( + IUpgradeSettingsFactory upgradeSettingsFactory, + IUpgradeService upgradeService, + IUmbracoMapper mapper) + { + _upgradeSettingsFactory = upgradeSettingsFactory; + _upgradeService = upgradeService; + _mapper = mapper; + } + + [HttpPost("authorize")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task Authorize() + { + await _upgradeService.Upgrade(); + return Ok(); + } + + [HttpGet("settings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(UpgradeSettingsViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + public async Task> Settings() + { + // TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async + // We want these to be async for future use (Ideally we'll have more async things), + // But we need to figure out how we want to handle it in the meantime? use Task.FromResult or? + UpgradeSettingsModel upgradeSettings = _upgradeSettingsFactory.GetUpgradeSettings(); + UpgradeSettingsViewModel viewModel = _mapper.Map(upgradeSettings)!; + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs new file mode 100644 index 0000000000..385cd1ff51 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Installer.Steps; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Infrastructure.Factories.Installer; +using Umbraco.New.Cms.Infrastructure.Installer.Steps; +using Umbraco.New.Cms.Web.Common.Installer; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class InstallerBuilderExtensions +{ + internal static IUmbracoBuilder AddNewInstaller(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + builder.WithCollectionBuilder() + .Add(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + builder.AddInstallSteps(); + services.AddTransient(); + + return builder; + } + + internal static IUmbracoBuilder AddUpgrader(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.AddTransient(); + builder.AddUpgradeSteps(); + services.AddTransient(); + + return builder; + } + + internal static IUmbracoBuilder AddInstallSteps(this IUmbracoBuilder builder) + { + builder.InstallSteps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return builder; + } + + public static NewInstallStepCollectionBuilder InstallSteps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + internal static IUmbracoBuilder AddUpgradeSteps(this IUmbracoBuilder builder) + { + builder.UpgradeSteps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return builder; + } + + public static UpgradeStepCollectionBuilder UpgradeSteps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs new file mode 100644 index 0000000000..0c6dfd8e93 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireRuntimeLevelAttribute : ActionFilterAttribute +{ + private readonly RuntimeLevel _requiredRuntimeLevel; + + public RequireRuntimeLevelAttribute(RuntimeLevel requiredRuntimeLevel) => + _requiredRuntimeLevel = requiredRuntimeLevel; + + public override void OnActionExecuting(ActionExecutingContext context) + { + IRuntimeState runtimeState = context.HttpContext.RequestServices.GetRequiredService(); + if (runtimeState.Level == _requiredRuntimeLevel) + { + return; + } + + // We're not in the expected runtime level, so we need to short circuit + var problemDetails = new ProblemDetails + { + Title = "Invalid runtime level", + Detail = $"Runtime level {_requiredRuntimeLevel} is required", + Status = StatusCodes.Status428PreconditionRequired, + Type = "Error", + }; + + context.Result = new ObjectResult(problemDetails) { StatusCode = StatusCodes.Status428PreconditionRequired }; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs new file mode 100644 index 0000000000..f0921f2244 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSwag.AspNetCore; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.ManagementApi.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi; + +public class ManagementApiComposer : IComposer +{ + private const string ApiTitle = "Umbraco Backoffice API"; + private const string ApiAllName = "All"; + + private ApiVersion DefaultApiVersion => new(1, 0); + + public void Compose(IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + builder + .AddNewInstaller() + .AddUpgrader(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = DefaultApiVersion; + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + options.AssumeDefaultVersionWhenUnspecified = true; + options.UseApiBehavior = false; + }); + + services.AddOpenApiDocument(options => + { + options.Title = ApiTitle; + options.Version = ApiAllName; + options.DocumentName = ApiAllName; + options.Description = "This shows all APIs available in this version of Umbraco - Including all the legacy apis that is available for backward compatibility"; + }); + + services.AddVersionedApiExplorer(options => + { + options.DefaultApiVersion = DefaultApiVersion; + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + options.AddApiVersionParametersWhenVersionNeutral = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + // Not super happy with this, but we need to know the UmbracoPath when registering the controller + // To be able to replace the route template token + GlobalSettings? globalSettings = + builder.Config.GetSection(Constants.Configuration.ConfigGlobal).Get(); + var backofficePath = globalSettings.UmbracoPath.TrimStart(Constants.CharArrays.TildeForwardSlash); + + services.AddControllers(options => + { + options.Conventions.Add(new UmbracoBackofficeToken(Constants.Web.AttributeRouting.BackOfficeToken, backofficePath)); + }); + + builder.Services.Configure(options => + { + options.AddFilter(new UmbracoPipelineFilter( + "BackofficeSwagger", + applicationBuilder => + { + applicationBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context => + { + Exception? exception = context.Features.Get()?.Error; + if (exception is null) + { + return; + } + + var response = new ProblemDetails + { + Title = exception.Message, + Detail = exception.StackTrace, + Status = StatusCodes.Status500InternalServerError, + Instance = exception.GetType().Name, + Type = "Error", + }; + await context.Response.WriteAsJsonAsync(response); + })); + }, + applicationBuilder => + { + IServiceProvider provider = applicationBuilder.ApplicationServices; + GlobalSettings? settings = provider.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = provider.GetRequiredService(); + var officePath = settings.GetBackOfficePath(hostingEnvironment); + + // serve documents (same as app.UseSwagger()) + applicationBuilder.UseOpenApi(config => + { + config.Path = $"{officePath}/swagger/{{documentName}}/swagger.json"; + }); + + // Serve Swagger UI + applicationBuilder.UseSwaggerUi3(config => + { + config.Path = officePath + "/swagger"; + config.SwaggerRoutes.Clear(); + var swaggerPath = $"{officePath}/swagger/{ApiAllName}/swagger.json"; + config.SwaggerRoutes.Add(new SwaggerUi3Route(ApiAllName, swaggerPath)); + }); + }, + applicationBuilder => + { + applicationBuilder.UseEndpoints(endpoints => + { + // Maps attribute routed controllers. + endpoints.MapControllers(); + }); + } + )); + }); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs new file mode 100644 index 0000000000..88c50a8715 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs @@ -0,0 +1,144 @@ +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.Cms.ManagementApi.Mapping.Installer; + +public class InstallerViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new InstallData(), Map); + mapper.Define((source, context) => new UserInstallData(), Map); + mapper.Define((source, context) => new DatabaseInstallData(), Map); + mapper.Define((source, context) => new DatabaseModel(), Map); + mapper.Define((source, context) => new DatabaseModel(), Map); + mapper.Define((source, context) => new InstallSettingsViewModel(), Map); + mapper.Define((source, context) => new UserSettingsViewModel(), Map); + mapper.Define((source, context) => new DatabaseSettingsModel(), Map); + mapper.Define((source, context) => new DatabaseSettingsViewModel(), Map); + mapper.Define((source, context) => new ConsentLevelViewModel(), Map); + mapper.Define((source, context) => new UpgradeSettingsViewModel(), Map); + } + + // Umbraco.Code.MapAll + private void Map(UpgradeSettingsModel source, UpgradeSettingsViewModel target, MapperContext context) + { + target.CurrentState = source.CurrentState; + target.NewState = source.NewState; + target.NewVersion = source.NewVersion.ToString(); + target.OldVersion = source.OldVersion.ToString(); + } + + // Umbraco.Code.MapAll + private void Map(DatabaseInstallViewModel source, DatabaseModel target, MapperContext context) + { + target.ConnectionString = source.ConnectionString; + target.DatabaseName = source.Name ?? string.Empty; + target.DatabaseProviderMetadataId = source.Id; + target.IntegratedAuth = source.UseIntegratedAuthentication; + target.Login = source.Username; + target.Password = source.Password; + target.ProviderName = source.ProviderName; + target.Server = source.Server!; + } + + // Umbraco.Code.MapAll + private static void Map(InstallViewModel source, InstallData target, MapperContext context) + { + target.TelemetryLevel = source.TelemetryLevel; + target.User = context.Map(source.User)!; + target.Database = context.Map(source.Database)!; + } + + // Umbraco.Code.MapAll + private static void Map(UserInstallViewModel source, UserInstallData target, MapperContext context) + { + target.Email = source.Email; + target.Name = source.Name; + target.Password = source.Password; + target.SubscribeToNewsletter = source.SubscribeToNewsletter; + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseInstallViewModel source, DatabaseInstallData target, MapperContext context) + { + target.Id = source.Id; + target.ProviderName = source.ProviderName; + target.Server = source.Server; + target.Name = source.Name; + target.Username = source.Username; + target.Password = source.Password; + target.UseIntegratedAuthentication = source.UseIntegratedAuthentication; + target.ConnectionString = source.ConnectionString; + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseInstallData source, DatabaseModel target, MapperContext context) + { + target.ConnectionString = source.ConnectionString; + target.DatabaseName = source.Name ?? string.Empty; + target.DatabaseProviderMetadataId = source.Id; + target.IntegratedAuth = source.UseIntegratedAuthentication; + target.Login = source.Username; + target.Password = source.Password; + target.ProviderName = source.ProviderName; + target.Server = source.Server!; + } + + // Umbraco.Code.MapAll + private static void Map(InstallSettingsModel source, InstallSettingsViewModel target, MapperContext context) + { + target.User = context.Map(source.UserSettings)!; + target.Databases = context.MapEnumerable(source.DatabaseSettings); + } + + // Umbraco.Code.MapAll + private static void Map(UserSettingsModel source, UserSettingsViewModel target, MapperContext context) + { + target.MinCharLength = source.PasswordSettings.MinCharLength; + target.MinNonAlphaNumericLength = source.PasswordSettings.MinNonAlphaNumericLength; + target.ConsentLevels = context.MapEnumerable(source.ConsentLevels); + } + + // Umbraco.Code.MapAll + private static void Map(IDatabaseProviderMetadata source, DatabaseSettingsModel target, MapperContext context) + { + target.DefaultDatabaseName = source.DefaultDatabaseName; + target.DisplayName = source.DisplayName; + target.Id = source.Id; + target.ProviderName = source.ProviderName ?? string.Empty; + target.RequiresConnectionTest = source.RequiresConnectionTest; + target.RequiresCredentials = source.RequiresCredentials; + target.RequiresServer = source.RequiresServer; + target.ServerPlaceholder = source.ServerPlaceholder ?? string.Empty; + target.SortOrder = source.SortOrder; + target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + target.IsConfigured = false; // Defaults to false, we'll set this to true if needed, + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseSettingsModel source, DatabaseSettingsViewModel target, MapperContext context) + { + target.DefaultDatabaseName = source.DefaultDatabaseName; + target.DisplayName = source.DisplayName; + target.Id = source.Id; + target.IsConfigured = source.IsConfigured; + target.ProviderName = source.ProviderName; + target.RequiresConnectionTest = source.RequiresConnectionTest; + target.RequiresCredentials = source.RequiresCredentials; + target.RequiresServer = source.RequiresServer; + target.ServerPlaceholder = source.ServerPlaceholder; + target.SortOrder = source.SortOrder; + target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + } + + // Umbraco.Code.MapAll + private static void Map(ConsentLevelModel source, ConsentLevelViewModel target, MapperContext context) + { + target.Description = source.Description; + target.Level = source.Level; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj new file mode 100644 index 0000000000..fb8c327221 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + nullable + Umbraco.Cms.ManagementApi + false + false + + + + + + + + + + + + + + + + all + + + diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs new file mode 100644 index 0000000000..2774f5ba2e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "consentLevels")] +public class ConsentLevelViewModel +{ + [DataMember(Name = "level")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public TelemetryLevel Level { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs new file mode 100644 index 0000000000..1bc2f4c3e9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "databaseInstall")] +public class DatabaseInstallViewModel +{ + [DataMember(Name = "id")] + [Required] + public Guid Id { get; init; } + + [DataMember(Name = "providerName")] + [Required] + public string? ProviderName { get; init; } + + [DataMember(Name = "server")] + public string? Server { get; init; } + + [DataMember(Name = "name")] + public string? Name { get; init; } + + [DataMember(Name = "username")] + public string? Username { get; init; } + + [DataMember(Name = "password")] + [PasswordPropertyText] + public string? Password { get; init; } + + [DataMember(Name = "useIntegratedAuthentication")] + public bool UseIntegratedAuthentication { get; init; } + + [DataMember(Name = "connectionString")] + public string? ConnectionString { get; init; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs new file mode 100644 index 0000000000..0d2c45f105 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "databaseSettings")] +public class DatabaseSettingsViewModel +{ + [DataMember(Name = "id")] + public Guid Id { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } = string.Empty; + + [DataMember(Name = "defaultDatabaseName")] + public string DefaultDatabaseName { get; set; } = string.Empty; + + [DataMember(Name = "providerName")] + public string ProviderName { get; set; } = string.Empty; + + [DataMember(Name = "isConfigured")] + public bool IsConfigured { get; set; } + + [DataMember(Name = "requiresServer")] + public bool RequiresServer { get; set; } + + [DataMember(Name = "serverPlaceholder")] + public string ServerPlaceholder { get; set; } = string.Empty; + + [DataMember(Name = "requiresCredentials")] + public bool RequiresCredentials { get; set; } + + [DataMember(Name = "supportsIntegratedAuthentication")] + public bool SupportsIntegratedAuthentication { get; set; } + + [DataMember(Name = "requiresConnectionTest")] + public bool RequiresConnectionTest { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs new file mode 100644 index 0000000000..156aa73e3e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "installSettings")] +public class InstallSettingsViewModel +{ + [DataMember(Name = "user")] + public UserSettingsViewModel User { get; set; } = null!; + + [DataMember(Name = "databases")] + public IEnumerable Databases { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs new file mode 100644 index 0000000000..ed815a521d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +public class InstallViewModel +{ + [DataMember(Name = "user")] + [Required] + public UserInstallViewModel User { get; init; } = null!; + + [DataMember(Name = "database")] + [Required] + public DatabaseInstallViewModel Database { get; init; } = null!; + + [DataMember(Name = "telemetryLevel")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public TelemetryLevel TelemetryLevel { get; init; } = TelemetryLevel.Basic; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs new file mode 100644 index 0000000000..8274246070 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "upgradeSettingsViewModel")] +public class UpgradeSettingsViewModel +{ + [DataMember(Name = "currentState")] + public string CurrentState { get; set; } = string.Empty; + + [DataMember(Name = "newState")] + public string NewState { get; set; } = string.Empty; + + [DataMember(Name = "newVersion")] + public string NewVersion { get; set; } = string.Empty; + + [DataMember(Name = "oldVersion")] + public string OldVersion { get; set; } = string.Empty; + + [DataMember(Name = "reportUrl")] + public string ReportUrl => + $"https://our.umbraco.com/contribute/releases/compare?from={OldVersion}&to={NewVersion}¬es=1"; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs new file mode 100644 index 0000000000..dbdb859f63 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +public class UserInstallViewModel +{ + [DataMember(Name = "name")] + [Required] + [StringLength(255)] + public string Name { get; init; } = null!; + + [DataMember(Name = "email")] + [Required] + [EmailAddress] + public string Email { get; init; } = null!; + + [DataMember(Name = "password")] + [Required] + [PasswordPropertyText] + public string Password { get; init; } = null!; + + [DataMember(Name = "subscribeToNewsletter")] + public bool SubscribeToNewsletter { get; init; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs new file mode 100644 index 0000000000..b2be9e88c9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "user")] +public class UserSettingsViewModel +{ + [DataMember(Name = "minCharLength")] + public int MinCharLength { get; set; } + + [DataMember(Name = "minNonAlphaNumericLength")] + public int MinNonAlphaNumericLength { get; set; } + + [DataMember(Name = "consentLevels")] + public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index 0dbc62fb49..112d556712 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -53,6 +53,11 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata /// public string GenerateConnectionString(DatabaseModel databaseModel) { + if (databaseModel.Server is null) + { + throw new ArgumentNullException(nameof(databaseModel.Server)); + } + var server = databaseModel.Server; var databaseName = databaseModel.DatabaseName; var user = databaseModel.Login; @@ -89,7 +94,7 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata server = $"{server},1433"; } - if (user.Contains("@") == false) + if (user?.Contains("@") == false) { var userDomain = server; diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 23e8febd18..c6c63108ff 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -16,6 +16,10 @@ + + + + $(ProjectDir)appsettings-schema.json $(ProjectDir)../JsonSchema/ diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index bfbe4e56d5..bbeae780d8 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -63,6 +63,11 @@ public static partial class Constants public const string AreaToken = "area"; } + public static class AttributeRouting + { + public const string BackOfficeToken = "umbracoBackOffice"; + } + public static class EmailTypes { public const string HealthCheck = "HealthCheck"; diff --git a/src/Umbraco.Core/Install/InstallException.cs b/src/Umbraco.Core/Install/InstallException.cs index 69e28db92c..fcb878c677 100644 --- a/src/Umbraco.Core/Install/InstallException.cs +++ b/src/Umbraco.Core/Install/InstallException.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install; diff --git a/src/Umbraco.Core/Install/InstallStatusTracker.cs b/src/Umbraco.Core/Install/InstallStatusTracker.cs index 5403ded3ae..f1f92ef46c 100644 --- a/src/Umbraco.Core/Install/InstallStatusTracker.cs +++ b/src/Umbraco.Core/Install/InstallStatusTracker.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Collections; +using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Serialization; @@ -9,6 +9,7 @@ namespace Umbraco.Cms.Core.Install; /// /// An internal in-memory status tracker for the current installation /// +[Obsolete("This will no longer be used with the new backoffice APi, instead all steps run in one go")] public class InstallStatusTracker { private static ConcurrentHashSet _steps = new(); diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index 40f54bab33..b6a08d55ae 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Install.Models; @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps; /// /// Represents a step in the installation that ensure all the required permissions on files and folders are correct. /// +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "Permissions", diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs index cb008bf77c..6db33486f5 100644 --- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; @@ -9,6 +9,7 @@ using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "TelemetryIdConfiguration", diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 763b69226e..c67b1fa5fb 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; @@ -8,6 +8,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps /// /// This step is purely here to show the button to commence the upgrade /// + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.Upgrade, "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")] public class UpgradeStep : InstallSetupStep { diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index eb892d9cee..b52fc84fa9 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -11,6 +11,8 @@ public class DatabaseModel [DataMember(Name = "providerName")] public string? ProviderName { get; set; } + // TODO: Make this nullable in V11 + // Server can be null, for instance when installing a SQLite database. [DataMember(Name = "server")] public string Server { get; set; } = null!; @@ -18,10 +20,10 @@ public class DatabaseModel public string DatabaseName { get; set; } = null!; [DataMember(Name = "login")] - public string Login { get; set; } = null!; + public string? Login { get; set; } [DataMember(Name = "password")] - public string Password { get; set; } = null!; + public string? Password { get; set; } [DataMember(Name = "integratedAuth")] public bool IntegratedAuth { get; set; } diff --git a/src/Umbraco.Core/Install/Models/InstallInstructions.cs b/src/Umbraco.Core/Install/Models/InstallInstructions.cs index c86307d9b0..caabf0561c 100644 --- a/src/Umbraco.Core/Install/Models/InstallInstructions.cs +++ b/src/Umbraco.Core/Install/Models/InstallInstructions.cs @@ -1,7 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "installInstructions", Namespace = "")] public class InstallInstructions { diff --git a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs index 650c746998..3b82cac3de 100644 --- a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs +++ b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs @@ -1,10 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; /// /// Returned to the UI for each installation step that is completed /// +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "result", Namespace = "")] public class InstallProgressResultModel { diff --git a/src/Umbraco.Core/Install/Models/InstallSetup.cs b/src/Umbraco.Core/Install/Models/InstallSetup.cs index 2a1e3ce9f7..8b3ce4bb97 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetup.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetup.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.Install.Models; /// Model containing all the install steps for setting up the UI /// [DataContract(Name = "installSetup", Namespace = "")] +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallSetup { public InstallSetup() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs index 3849a09d75..a256a23436 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs @@ -1,8 +1,9 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; /// /// The object returned from each installation step /// +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallSetupResult { public InstallSetupResult() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs index a9d24447c6..2fe3d9814f 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Install.Models; @@ -7,6 +7,7 @@ namespace Umbraco.Cms.Core.Install.Models; /// Model to give to the front-end to collect the information for each step /// [DataContract(Name = "step", Namespace = "")] +[Obsolete("Will be replaced with IInstallStep in the new backoffice API")] public abstract class InstallSetupStep : InstallSetupStep { /// @@ -30,6 +31,7 @@ public abstract class InstallSetupStep : InstallSetupStep } [DataContract(Name = "step", Namespace = "")] +[Obsolete("Will be replaced with IInstallStep in the new backoffice API")] public abstract class InstallSetupStep { protected InstallSetupStep() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs index c6d0657d33..63edcf0942 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs @@ -1,5 +1,6 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the use of IInstallStep in the new backoffice API")] public sealed class InstallSetupStepAttribute : Attribute { public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) diff --git a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs index 74170857b5..70dc08b39c 100644 --- a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs +++ b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs @@ -1,5 +1,6 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallTrackingItem { public InstallTrackingItem(string name, int serverOrder) diff --git a/src/Umbraco.Core/Install/Models/InstallationType.cs b/src/Umbraco.Core/Install/Models/InstallationType.cs index b2b6a428fa..a2e6c92bad 100644 --- a/src/Umbraco.Core/Install/Models/InstallationType.cs +++ b/src/Umbraco.Core/Install/Models/InstallationType.cs @@ -1,5 +1,6 @@ namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("This will no longer be used with the new backoffice APi, install steps and upgrade steps is instead two different interfaces.")] [Flags] public enum InstallationType { diff --git a/src/Umbraco.Core/Install/Models/Package.cs b/src/Umbraco.Core/Install/Models/Package.cs index 9ac30ab9a7..f85e4b1f67 100644 --- a/src/Umbraco.Core/Install/Models/Package.cs +++ b/src/Umbraco.Core/Install/Models/Package.cs @@ -1,7 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("This is no longer used, instead PackageDefinition and InstalledPackage is used")] [DataContract(Name = "package")] public class Package { diff --git a/src/Umbraco.Core/Install/Models/UserModel.cs b/src/Umbraco.Core/Install/Models/UserModel.cs index 61f76c795d..debae20806 100644 --- a/src/Umbraco.Core/Install/Models/UserModel.cs +++ b/src/Umbraco.Core/Install/Models/UserModel.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "user", Namespace = "")] public class UserModel { diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 7b711f8750..c07e48705b 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Infrastructure.Install.InstallSteps; namespace Umbraco.Cms.Infrastructure.Install; +[Obsolete("This will be replaced with an ordered collection with the new backoffice")] public sealed class InstallStepCollection { private readonly InstallHelper _installHelper; @@ -17,9 +18,12 @@ public sealed class InstallStepCollection InstallSetupStep[] a = installerSteps.ToArray(); _orderedInstallerSteps = new InstallSetupStep[] { - a.OfType().First(), a.OfType().First(), - a.OfType().First(), a.OfType().First(), - a.OfType().First(), a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), a.OfType().First(), // TODO: Add these back once we have a compatible Starter kit diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs index d212909a9f..c67d1f64b0 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Install.Models; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "UmbracoVersion", diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 87be3c6e8f..8d2886c223 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -9,6 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] public class DatabaseConfigureStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 42712f20bd..f328fea676 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] public class DatabaseInstallStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index fa35ee5b07..4039533fa1 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -13,6 +13,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, "DatabaseUpgrade", 12, "")] public class DatabaseUpgradeStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 2ebc756dc2..cf984aed59 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -29,6 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps /// error, etc... and the end-user refreshes the installer then we cannot show the user screen because they've already entered that information so instead we'll /// display a simple continue installation view. /// + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] public class NewInstallStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 7d48468a96..93219af4c9 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -16,7 +16,6 @@ TRACE_SCOPES; - @@ -111,6 +110,9 @@ <_Parameter1>DynamicProxyGenAssembly2 + + <_Parameter1>Umbraco.New.Cms.Infrastructure + diff --git a/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs new file mode 100644 index 0000000000..c71ce126d8 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs @@ -0,0 +1,16 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +/// +/// Creates based on the currently configured providers. +/// +public interface IDatabaseSettingsFactory +{ + /// + /// Creates a collection of database settings models for the currently installed database providers + /// + /// Collection of database settings. + /// Thrown if a connection string is preconfigured, but provider name is missing. + ICollection GetDatabaseSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs new file mode 100644 index 0000000000..552cd0af2b --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IInstallSettingsFactory +{ + InstallSettingsModel GetInstallSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs new file mode 100644 index 0000000000..45daf3dcc1 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IUpgradeSettingsFactory +{ + UpgradeSettingsModel GetUpgradeSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs new file mode 100644 index 0000000000..5c069d7084 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IUserSettingsFactory +{ + UserSettingsModel GetUserSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs new file mode 100644 index 0000000000..5d57b1554d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs @@ -0,0 +1,24 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class InstallSettingsFactory : IInstallSettingsFactory +{ + private readonly IUserSettingsFactory _userSettingsFactory; + private readonly IDatabaseSettingsFactory _databaseSettingsFactory; + + public InstallSettingsFactory( + IUserSettingsFactory userSettingsFactory, + IDatabaseSettingsFactory databaseSettingsFactory) + { + _userSettingsFactory = userSettingsFactory; + _databaseSettingsFactory = databaseSettingsFactory; + } + + public InstallSettingsModel GetInstallSettings() => + new() + { + DatabaseSettings = _databaseSettingsFactory.GetDatabaseSettings(), + UserSettings = _userSettingsFactory.GetUserSettings(), + }; +} diff --git a/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs new file mode 100644 index 0000000000..314e83a995 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs @@ -0,0 +1,34 @@ +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class UpgradeSettingsFactory : IUpgradeSettingsFactory +{ + private readonly IRuntimeState _runtimeState; + private readonly IUmbracoVersion _umbracoVersion; + + public UpgradeSettingsFactory( + IRuntimeState runtimeState, + IUmbracoVersion umbracoVersion) + { + _runtimeState = runtimeState; + _umbracoVersion = umbracoVersion; + } + + + public UpgradeSettingsModel GetUpgradeSettings() + { + var model = new UpgradeSettingsModel + { + CurrentState = _runtimeState.CurrentMigrationState ?? string.Empty, + NewState = _runtimeState.FinalMigrationState ?? string.Empty, + NewVersion = _umbracoVersion.SemanticVersion, + OldVersion = new SemVersion(_umbracoVersion.SemanticVersion.Major), // TODO can we find the old version somehow? e.g. from current state + }; + + return model; + } +} diff --git a/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs new file mode 100644 index 0000000000..9386cf713a --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class UserSettingsFactory : IUserSettingsFactory +{ + private readonly ILocalizedTextService _localizedTextService; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + + public UserSettingsFactory( + IOptions securitySettings, + ILocalizedTextService localizedTextService) + { + _localizedTextService = localizedTextService; + _passwordConfiguration = securitySettings.Value; + } + + public UserSettingsModel GetUserSettings() => + new() + { + PasswordSettings = CreatePasswordSettingsModel(), + ConsentLevels = CreateConsentLevelModels(), + }; + + private PasswordSettingsModel CreatePasswordSettingsModel() => + new() + { + MinCharLength = _passwordConfiguration.RequiredLength, + MinNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars() + }; + + private IEnumerable CreateConsentLevelModels() => + Enum.GetValues() + .ToList() + .Select(level => new ConsentLevelModel + { + Level = level, + Description = GetTelemetryLevelDescription(level), + }); + + private string GetTelemetryLevelDescription(TelemetryLevel telemetryLevel) => telemetryLevel switch + { + TelemetryLevel.Minimal => _localizedTextService.Localize("analytics", "minimalLevelDescription"), + TelemetryLevel.Basic => _localizedTextService.Localize("analytics", "basicLevelDescription"), + TelemetryLevel.Detailed => _localizedTextService.Localize("analytics", "detailedLevelDescription"), + _ => throw new ArgumentOutOfRangeException(nameof(telemetryLevel), $"Did not expect telemetry level of {telemetryLevel}") + }; +} diff --git a/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs b/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs new file mode 100644 index 0000000000..3344b9ffb0 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs @@ -0,0 +1,23 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer; + +/// +/// Defines a step that's required to install Umbraco. +/// +public interface IInstallStep +{ + /// + /// Executes the install step. + /// + /// InstallData model containing the data provided by the installer UI. + /// + Task ExecuteAsync(InstallData model); + + /// + /// Determines if the step is required to execute. + /// + /// InstallData model containing the data provided by the installer UI. + /// True if the step should execute, otherwise false. + Task RequiresExecutionAsync(InstallData model); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs b/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs new file mode 100644 index 0000000000..6f52aca6ec --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs @@ -0,0 +1,18 @@ +namespace Umbraco.New.Cms.Core.Installer; + +/// +/// Defines a step that's required to upgrade Umbraco. +/// +public interface IUpgradeStep +{ + /// + /// Executes the upgrade step. + /// + Task ExecuteAsync(); + + /// + /// Determines if the step is required to execute. + /// + /// True if the step should execute, otherwise false. + Task RequiresExecutionAsync(); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs new file mode 100644 index 0000000000..7077b85a28 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.New.Cms.Core.Installer; + +public class NewInstallStepCollection : BuilderCollectionBase +{ + public NewInstallStepCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs new file mode 100644 index 0000000000..d3c572b7b7 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class NewInstallStepCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override NewInstallStepCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs new file mode 100644 index 0000000000..37574c91e1 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs @@ -0,0 +1,47 @@ +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class FilePermissionsStep : IInstallStep, IUpgradeStep +{ + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _localizedTextService; + + public FilePermissionsStep( + IFilePermissionHelper filePermissionHelper, + ILocalizedTextService localizedTextService) + { + _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + // validate file permissions + var permissionsOk = + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> report); + + var translatedErrors = + report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); + if (permissionsOk == false) + { + throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private static Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs new file mode 100644 index 0000000000..cacce0d763 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class RestartRuntimeStep : IInstallStep, IUpgradeStep +{ + private readonly IRuntime _runtime; + + public RestartRuntimeStep(IRuntime runtime) => _runtime = runtime; + + public async Task ExecuteAsync(InstallData _) => await Execute(); + + public async Task ExecuteAsync() => await Execute(); + + private async Task Execute() => await _runtime.RestartAsync(); + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs new file mode 100644 index 0000000000..a05a8228fe --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep +{ + private readonly IOptions _globalSettings; + private readonly ISiteIdentifierService _siteIdentifierService; + + public TelemetryIdentifierStep( + IOptions globalSettings, + ISiteIdentifierService siteIdentifierService) + { + _globalSettings = globalSettings; + _siteIdentifierService = siteIdentifierService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + _siteIdentifierService.TryCreateSiteIdentifier(out _); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + { + // Verify that Json value is not empty string + // Try & get a value stored in appSettings.json + var backofficeIdentifierRaw = _globalSettings.Value.Id; + + // No need to add Id again if already found + return Task.FromResult(string.IsNullOrEmpty(backofficeIdentifierRaw)); + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs new file mode 100644 index 0000000000..1deb06681d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class UpgradeStepCollection : BuilderCollectionBase +{ + public UpgradeStepCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs new file mode 100644 index 0000000000..a7b2b803ce --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class UpgradeStepCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override UpgradeStepCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs new file mode 100644 index 0000000000..a3687814c3 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class ConsentLevelModel +{ + public TelemetryLevel Level { get; set; } + + public string Description { get; set; } = string.Empty; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs new file mode 100644 index 0000000000..6141ea7a9f --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs @@ -0,0 +1,20 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class DatabaseInstallData +{ + public Guid Id { get; set; } + + public string? ProviderName { get; set; } + + public string? Server { get; set; } + + public string? Name { get; set; } + + public string? Username { get; set; } + + public string? Password { get; set; } + + public bool UseIntegratedAuthentication { get; set; } + + public string? ConnectionString { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs new file mode 100644 index 0000000000..2f8aabb8af --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs @@ -0,0 +1,26 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class DatabaseSettingsModel +{ + public Guid Id { get; set; } + + public int SortOrder { get; set; } + + public string DisplayName { get; set; } = string.Empty; + + public string DefaultDatabaseName { get; set; } = string.Empty; + + public string ProviderName { get; set; } = string.Empty; + + public bool IsConfigured { get; set; } + + public bool RequiresServer { get; set; } + + public string ServerPlaceholder { get; set; } = string.Empty; + + public bool RequiresCredentials { get; set; } + + public bool SupportsIntegratedAuthentication { get; set; } + + public bool RequiresConnectionTest { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs new file mode 100644 index 0000000000..2283cf2482 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class InstallData +{ + public UserInstallData User { get; set; } = null!; + + public DatabaseInstallData Database { get; set; } = null!; + + public TelemetryLevel TelemetryLevel { get; set; } = TelemetryLevel.Basic; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs new file mode 100644 index 0000000000..6b0aeb370d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class InstallSettingsModel +{ + public UserSettingsModel UserSettings { get; set; } = null!; + + public ICollection DatabaseSettings { get; set; } = new List(); +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs new file mode 100644 index 0000000000..2efec3a696 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class PasswordSettingsModel +{ + public int MinCharLength { get; set; } + + public int MinNonAlphaNumericLength { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs new file mode 100644 index 0000000000..b403367548 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Semver; +using Umbraco.Extensions; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UpgradeSettingsModel +{ + public string CurrentState { get; set; } = string.Empty; + + public string NewState { get; set; } = string.Empty; + + public SemVersion NewVersion { get; set; } = null!; + + public SemVersion OldVersion { get; set; } = null!; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs new file mode 100644 index 0000000000..18865565df --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs @@ -0,0 +1,12 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UserInstallData +{ + public string Name { get; set; } = null!; + + public string Email { get; set; } = null!; + + public string Password { get; set; } = null!; + + public bool SubscribeToNewsletter { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs new file mode 100644 index 0000000000..2db9f04b65 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UserSettingsModel +{ + public PasswordSettingsModel PasswordSettings { get; set; } = null!; + + public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs new file mode 100644 index 0000000000..c5dc499d62 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs @@ -0,0 +1,14 @@ +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public interface IInstallService +{ + /// + /// Runs all the steps in the , installing Umbraco + /// + /// InstallData containing the required data used to install + /// + Task Install(InstallData model); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs new file mode 100644 index 0000000000..f6eefa8a85 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs @@ -0,0 +1,11 @@ +using Umbraco.New.Cms.Core.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public interface IUpgradeService +{ + /// + /// Runs all the steps in the , upgrading Umbraco. + /// + Task Upgrade(); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs new file mode 100644 index 0000000000..98813cdaec --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public class InstallService : IInstallService +{ + private readonly ILogger _logger; + private readonly NewInstallStepCollection _installSteps; + private readonly IRuntimeState _runtimeState; + + public InstallService( + ILogger logger, + NewInstallStepCollection installSteps, + IRuntimeState runtimeState) + { + _logger = logger; + _installSteps = installSteps; + _runtimeState = runtimeState; + } + + /// + public async Task Install(InstallData model) + { + if (_runtimeState.Level != RuntimeLevel.Install) + { + throw new InvalidOperationException($"Runtime level must be Install to install but was: {_runtimeState.Level}"); + } + + try + { + await RunSteps(model); + } + catch (Exception exception) + { + _logger.LogError(exception, "Encountered an error when running the install steps"); + throw; + } + } + + private async Task RunSteps(InstallData model) + { + foreach (IInstallStep step in _installSteps) + { + var stepName = step.GetType().Name; + _logger.LogInformation("Checking if {StepName} requires execution", stepName); + if (await step.RequiresExecutionAsync(model) is false) + { + _logger.LogInformation("Skipping {StepName}", stepName); + continue; + } + + _logger.LogInformation("Running {StepName}", stepName); + await step.ExecuteAsync(model); + _logger.LogInformation("Finished {StepName}", stepName); + } + } +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs new file mode 100644 index 0000000000..6f11e8a7ac --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public class UpgradeService : IUpgradeService +{ + private readonly UpgradeStepCollection _upgradeSteps; + private readonly IRuntimeState _runtimeState; + private readonly ILogger _logger; + + public UpgradeService( + UpgradeStepCollection upgradeSteps, + IRuntimeState runtimeState, + ILogger logger) + { + _upgradeSteps = upgradeSteps; + _runtimeState = runtimeState; + _logger = logger; + } + + /// + public async Task Upgrade() + { + if (_runtimeState.Level != RuntimeLevel.Upgrade) + { + throw new InvalidOperationException( + $"Runtime level must be Upgrade to upgrade but was: {_runtimeState.Level}"); + } + + try + { + await RunSteps(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Encountered an error when running the upgrade steps"); + throw; + } + } + + private async Task RunSteps() + { + foreach (IUpgradeStep step in _upgradeSteps) + { + var stepName = step.GetType().Name; + _logger.LogInformation("Checking if {StepName} requires execution", stepName); + if (await step.RequiresExecutionAsync() is false) + { + _logger.LogInformation("Skipping {StepName}", stepName); + continue; + } + + _logger.LogInformation("Running {StepName}", stepName); + await step.ExecuteAsync(); + _logger.LogInformation("Finished {StepName}", stepName); + } + } +} diff --git a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj new file mode 100644 index 0000000000..e7c38a23af --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + Umbraco.New.Cms.Core + false + nullable + false + + + + + + + diff --git a/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs new file mode 100644 index 0000000000..be941104b6 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Factories.Installer; + +public class DatabaseSettingsFactory : IDatabaseSettingsFactory +{ + private readonly IEnumerable _databaseProviderMetadata; + private readonly IOptionsMonitor _connectionStrings; + private readonly IUmbracoMapper _mapper; + + public DatabaseSettingsFactory( + IEnumerable databaseProviderMetadata, + IOptionsMonitor connectionStrings, + IUmbracoMapper mapper) + { + _databaseProviderMetadata = databaseProviderMetadata; + _connectionStrings = connectionStrings; + _mapper = mapper; + } + + /// + public ICollection GetDatabaseSettings() + { + ConnectionStrings? connectionString = _connectionStrings.CurrentValue; + + // If the connection string is configured we just return the configured provider. + if (connectionString.IsConnectionStringConfigured()) + { + var providerName = connectionString.ProviderName; + IDatabaseProviderMetadata? providerMetaData = _databaseProviderMetadata + .FirstOrDefault(x => x.ProviderName?.Equals(providerName, StringComparison.InvariantCultureIgnoreCase) ?? false); + + if (providerMetaData is null) + { + throw new InvalidOperationException($"Provider {providerName} is not a registered provider"); + } + + DatabaseSettingsModel configuredProvider = _mapper.Map(providerMetaData)!; + + configuredProvider.IsConfigured = true; + + return new[] { configuredProvider }; + } + + List providers = _mapper.MapEnumerable(_databaseProviderMetadata); + return providers; + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs new file mode 100644 index 0000000000..38faddff09 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs @@ -0,0 +1,176 @@ +using System.Collections.Specialized; +using System.Data.Common; +using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; +using Constants = Umbraco.Cms.Core.Constants; +using HttpResponseMessage = System.Net.Http.HttpResponseMessage; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class CreateUserStep : IInstallStep +{ + private readonly IUserService _userService; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IHttpClientFactory _httpClientFactory; + private readonly SecuritySettings _securitySettings; + private readonly IOptionsMonitor _connectionStrings; + private readonly ICookieManager _cookieManager; + private readonly IBackOfficeUserManager _userManager; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IMetricsConsentService _metricsConsentService; + + public CreateUserStep( + IUserService userService, + DatabaseBuilder databaseBuilder, + IHttpClientFactory httpClientFactory, + IOptions securitySettings, + IOptionsMonitor connectionStrings, + ICookieManager cookieManager, + IBackOfficeUserManager userManager, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IMetricsConsentService metricsConsentService) + { + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _httpClientFactory = httpClientFactory; + _securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings)); + _connectionStrings = connectionStrings; + _cookieManager = cookieManager; + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _metricsConsentService = metricsConsentService; + } + + public async Task ExecuteAsync(InstallData model) + { + IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } + + UserInstallData user = model.User; + admin.Email = user.Email.Trim(); + admin.Name = user.Name.Trim(); + admin.Username = user.Email.Trim(); + + _userService.Save(admin); + + BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + { + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } + + IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } + + _metricsConsentService.SetConsentLevel(model.TelemetryLevel); + + if (model.User.SubscribeToNewsletter) + { + var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } }; + var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); + + HttpClient httpClient = _httpClientFactory.CreateClient(); + + try + { + HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; + } + catch { /* fail in silence */ } + } + } + + /// + public Task RequiresExecutionAsync(InstallData model) + { + InstallState installState = GetInstallState(); + if (installState.HasFlag(InstallState.Unknown)) + { + // In this one case when it's a brand new install and nothing has been configured, make sure the + // back office cookie is cleared so there's no old cookies lying around causing problems + _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); + } + + var shouldRun = installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.HasNonDefaultUser); + return Task.FromResult(shouldRun); + } + + private InstallState GetInstallState() + { + InstallState installState = InstallState.Unknown; + + if (_databaseBuilder.IsDatabaseConfigured) + { + installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; + } + + ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; + + var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); + if (isConnectionStringConfigured) + { + installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; + } + + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); + var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); + if (isConnectionAvailable) + { + installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; + } + + var isUmbracoInstalled = isConnectionAvailable && _databaseBuilder.IsUmbracoInstalled(); + if (isUmbracoInstalled) + { + installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; + } + + var hasSomeNonDefaultUser = isUmbracoInstalled && _databaseBuilder.HasSomeNonDefaultUser(); + if (hasSomeNonDefaultUser) + { + installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; + } + + return installState; + } + + [Flags] + private enum InstallState : short + { + // This is an easy way to avoid 0 enum assignment and not worry about + // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags + Unknown = 1, + HasVersion = 1 << 1, + HasConnectionString = 1 << 2, + ConnectionStringConfigured = 1 << 3, + CanConnect = 1 << 4, + UmbracoInstalled = 1 << 5, + HasNonDefaultUser = 1 << 6 + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs new file mode 100644 index 0000000000..dd78e149e5 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseConfigureStep : IInstallStep +{ + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly ILogger _logger; + private readonly IUmbracoMapper _mapper; + + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IUmbracoMapper mapper) + { + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _logger = logger; + _mapper = mapper; + } + + public Task ExecuteAsync(InstallData model) + { + DatabaseModel databaseModel = _mapper.Map(model.Database)!; + + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseModel, false)) + { + throw new InstallException("Could not connect to the database"); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) + { + // If the connection string is already present in config we don't need to configure it again + if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) + { + try + { + // Since a connection string was present we verify the db can connect and query + _databaseBuilder.ValidateSchema(); + + return Task.FromResult(false); + } + catch (Exception ex) + { + // Something went wrong, could not connect so probably need to reconfigure + _logger.LogError(ex, "An error occurred, reconfiguring..."); + + return Task.FromResult(true); + } + } + + return Task.FromResult(true); + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs new file mode 100644 index 0000000000..9abe6823ab --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseInstallStep : IInstallStep, IUpgradeStep +{ + private readonly IRuntimeState _runtime; + private readonly DatabaseBuilder _databaseBuilder; + + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) + { + _runtime = runtime; + _databaseBuilder = databaseBuilder; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + + DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); + + if (result?.Success == false) + { + throw new InstallException("The database failed to install. ERROR: " + result.Message); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs new file mode 100644 index 0000000000..83cae8d80b --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep +{ + private readonly DatabaseBuilder _databaseBuilder; + private readonly IRuntimeState _runtime; + private readonly ILogger _logger; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IKeyValueService _keyValueService; + + public DatabaseUpgradeStep( + DatabaseBuilder databaseBuilder, + IRuntimeState runtime, + ILogger logger, + IUmbracoVersion umbracoVersion, + IKeyValueService keyValueService) + { + _databaseBuilder = databaseBuilder; + _runtime = runtime; + _logger = logger; + _umbracoVersion = umbracoVersion; + _keyValueService = keyValueService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + _logger.LogInformation("Running 'Upgrade' service"); + + var plan = new UmbracoPlan(_umbracoVersion); + plan.AddPostMigration(); // needed when running installer (back-office) + + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + + if (result?.Success == false) + { + throw new InstallException("The database failed to upgrade. ERROR: " + result.Message); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + { + // Don't do anything if RunTimeLevel is not Install/Upgrade + if (_runtime.Level == RuntimeLevel.Run) + { + return Task.FromResult(false); + } + + // Check the upgrade state, if it matches we dont have to upgrade. + var plan = new UmbracoPlan(_umbracoVersion); + var currentState = _keyValueService.GetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name); + if (currentState != plan.FinalState) + { + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs new file mode 100644 index 0000000000..53989bf3b7 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Infrastructure.Install; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class RegisterInstallCompleteStep : IInstallStep, IUpgradeStep +{ + private readonly InstallHelper _installHelper; + + public RegisterInstallCompleteStep(InstallHelper installHelper) => _installHelper = installHelper; + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() => _installHelper.SetInstallStatusAsync(true, string.Empty); + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private static Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj new file mode 100644 index 0000000000..27e27cc981 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + Umbraco.New.Cms.Infrastructure + false + nullable + false + + + + + + + + diff --git a/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs new file mode 100644 index 0000000000..62954c61e0 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Web.Common.Installer; + +public class SignInUserStep : IInstallStep +{ + private readonly IBackOfficeSignInManager _backOfficeSignInManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; + + public SignInUserStep( + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager) + { + _backOfficeSignInManager = backOfficeSignInManager; + _backOfficeUserManager = backOfficeUserManager; + } + + public InstallationType InstallationTypeTarget => InstallationType.NewInstall; + + public async Task ExecuteAsync(InstallData model) + { + BackOfficeIdentityUser identityUser = await _backOfficeUserManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + await _backOfficeSignInManager.SignInAsync(identityUser, false); + } + + public Task RequiresExecutionAsync(InstallData model) => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs b/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs new file mode 100644 index 0000000000..44b1c27e2d --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; + +namespace Umbraco.New.Cms.Web.Common.Routing; + +/// +/// Routes a controller within the backoffice area, I.E /umbraco +/// +public class BackOfficeRouteAttribute : RouteAttribute +{ + // All this does is append [umbracoBackoffice]/ to the route, + // this is then replaced with whatever is configures as UmbracoPath by the UmbracoBackofficeToken convention + public BackOfficeRouteAttribute(string template) + : base($"[{Constants.Web.AttributeRouting.BackOfficeToken}]/" + template.TrimStart('/')) + { + } +} diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs b/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs new file mode 100644 index 0000000000..b8e5e45c00 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs @@ -0,0 +1,42 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Umbraco.New.Cms.Web.Common.Routing; + +/// +/// Adds a custom template token for specifying backoffice route with attribute routing +/// +// Adapted from https://stackoverflow.com/questions/68911881/asp-net-core-api-add-custom-route-token-resolver +public class UmbracoBackofficeToken : IApplicationModelConvention +{ + private readonly string _umbracoPath; + private readonly string _tokenRegex; + + public UmbracoBackofficeToken(string tokenName, string umbracoPath) + { + _umbracoPath = umbracoPath; + _tokenRegex = $@"(\[{tokenName}])(? actionModel.Selectors), _umbracoPath); + } + } + + private void UpdateSelectors(IEnumerable selectors, string tokenValue) + { + foreach (SelectorModel selector in selectors.Where(s => s.AttributeRouteModel is not null)) + { + // We just checked that AttributeRouteModel is not null, so silence the nullable warning + selector.AttributeRouteModel!.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue); + selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue); + } + } + + private string? InsertTokenValue(string? template, string tokenValue) + => template is null ? template : Regex.Replace(template, _tokenRegex, tokenValue); +} diff --git a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj new file mode 100644 index 0000000000..159537c9d8 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + false + nullable + false + + + + + + + + diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index 3dfc9f51b1..e5f1c4fc36 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -18,6 +18,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; +[Obsolete("Will be replaced with a new API controller in the new backoffice api")] [UmbracoApiController] [AngularJsonOnlyConfiguration] [InstallAuthorize] diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs index 26eb3e9302..590fb73e0e 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs @@ -8,6 +8,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; +[Obsolete("Will be replaced with attribute routing in the new backoffice API")] public class InstallAreaRoutes : IAreaRoutes { private readonly IHostingEnvironment _hostingEnvironment; @@ -40,7 +41,6 @@ public class InstallAreaRoutes : IAreaRoutes ControllerExtensions.GetControllerName(), Constants.Web.Mvc.InstallArea); - break; case RuntimeLevel.Run: diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index ab6029cc43..c8af0d8ba8 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install; /// /// The Installation controller /// +[Obsolete("Will no longer be required with the new backoffice API")] [InstallAuthorize] [Area(Constants.Web.Mvc.InstallArea)] public class InstallController : Controller diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 3f24e2717e..eb3a5c5f01 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -49,6 +49,10 @@ + + + + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs new file mode 100644 index 0000000000..9bf039af66 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Filters; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.ManagementApi.Filters; + +[TestFixture] +public class RequireRuntimeLevelAttributeTest +{ + [Test] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Run, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Unknown, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Boot, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Upgrade, true)] + [TestCase(RuntimeLevel.Run, RuntimeLevel.Upgrade, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Install, false)] + [TestCase(RuntimeLevel.Upgrade, RuntimeLevel.Upgrade, false)] + public void BlocksWhenIncorrectRuntime(RuntimeLevel requiredLevel, RuntimeLevel actualLevel, bool shouldFail) + { + var executionContext = CreateActionExecutingContext(actualLevel); + + var sut = new RequireRuntimeLevelAttribute(requiredLevel); + sut.OnActionExecuting(executionContext); + + if (shouldFail) + { + AssertFailure(executionContext); + return; + } + + // Assert success, result being null == we haven't short circuited. + Assert.IsNull(executionContext.Result); + } + + private void AssertFailure(ActionExecutingContext executionContext) + { + var result = executionContext.Result; + Assert.IsInstanceOf(result); + + var objectResult = (ObjectResult)result; + + Assert.AreEqual(StatusCodes.Status428PreconditionRequired, objectResult?.StatusCode); + Assert.IsInstanceOf(objectResult?.Value); + } + + private ActionExecutingContext CreateActionExecutingContext(RuntimeLevel targetRuntimeLevel) + { + var actionContext = new ActionContext() + { + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + ActionDescriptor = new ActionDescriptor() + }; + + var executingContext = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + new()); + + var fakeRuntime = new Mock(); + fakeRuntime.Setup(x => x.Level).Returns(targetRuntimeLevel); + + var fakeServiceProvider = new Mock(); + fakeServiceProvider.Setup(x => x.GetService(typeof(IRuntimeState))).Returns(fakeRuntime.Object); + actionContext.HttpContext.RequestServices = fakeServiceProvider.Object; + + return executingContext; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs index 53270a5ac2..104831f025 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs @@ -24,10 +24,10 @@ public class DistributedCacheTests var cacheRefresherCollection = new CacheRefresherCollection(() => new[] { new TestCacheRefresher() }); - _distributedCache = new Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection); + _distributedCache = new global::Umbraco.Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection); } - private Cms.Core.Cache.DistributedCache _distributedCache; + private global::Umbraco.Cms.Core.Cache.DistributedCache _distributedCache; private IServerRoleAccessor ServerRegistrar { get; set; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs index 47fcce82ee..a9748a5484 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs @@ -346,7 +346,7 @@ public class DefaultShortStringHelperTestsWithoutSetup public void Utf8ToAsciiConverter() { const string str = "a\U00010F00z\uA74Ftéô"; - var output = Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str); + var output = global::Umbraco.Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str); Assert.AreEqual("a?zooteo", output); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs new file mode 100644 index 0000000000..cd40293bfd --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services; + +[TestFixture] +public class InstallServiceTests +{ + [Test] + public void RequiresInstallRuntimeToInstall() + { + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); + var stepCollection = new NewInstallStepCollection(Enumerable.Empty); + + var sut = new InstallService(Mock.Of>(), stepCollection, runtimeStateMock.Object); + + Assert.ThrowsAsync(async () => await sut.Install(new InstallData())); + } + + [Test] + public async Task OnlyRunsStepsThatRequireExecution() + { + var steps = new[] + { + new TestInstallStep { ShouldRun = true }, + new TestInstallStep { ShouldRun = false }, + new TestInstallStep { ShouldRun = true }, + }; + + var sut = CreateInstallService(steps); + await sut.Install(new InstallData()); + + foreach (var step in steps) + { + Assert.AreEqual(step.ShouldRun, step.HasRun); + } + } + + [Test] + public async Task StepsRunInCollectionOrder() + { + List runOrder = new List(); + + var steps = new[] + { + new TestInstallStep { Id = 1 }, + new TestInstallStep { Id = 2 }, + new TestInstallStep { Id = 3 }, + }; + + // Add an method delegate that will add the step itself, that way we can know the executed order. + foreach (var step in steps) + { + step.AdditionalExecution = _ => + { + runOrder.Add(step); + return Task.CompletedTask; + }; + } + + var sut = CreateInstallService(steps); + await sut.Install(new InstallData()); + + // The ID's are strictly not necessary, but it makes potential debugging easier. + var expectedRunOrder = steps.Select(x => x.Id); + var actualRunOrder = runOrder.Select(x => x.Id); + Assert.AreEqual(expectedRunOrder, actualRunOrder); + } + + private InstallService CreateInstallService(IEnumerable steps) + { + var logger = Mock.Of>(); + var stepCollection = new NewInstallStepCollection(() => steps); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Install); + + return new InstallService(logger, stepCollection, runtimeStateMock.Object); + } + + private class TestInstallStep : IInstallStep + { + public bool HasRun; + + public bool ShouldRun = true; + + public int Id; + + public Func AdditionalExecution; + + public Task ExecuteAsync(InstallData model) + { + HasRun = true; + + AdditionalExecution?.Invoke(model); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => Task.FromResult(ShouldRun); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs new file mode 100644 index 0000000000..d2934f9c81 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using UpgradeService = Umbraco.New.Cms.Core.Services.Installer.UpgradeService; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services; + +[TestFixture] +public class UpgradeServiceTests +{ + + [Test] + [TestCase(RuntimeLevel.Install)] + [TestCase(RuntimeLevel.Boot)] + [TestCase(RuntimeLevel.Run)] + [TestCase(RuntimeLevel.Unknown)] + public void RequiresUpgradeRuntimeToUpgrade(RuntimeLevel level) + { + var sut = CreateUpgradeService(Enumerable.Empty(), level); + + Assert.ThrowsAsync(async () => await sut.Upgrade()); + } + + [Test] + public async Task OnlyRunsStepsThatRequireExecution() + { + var steps = new[] + { + new TestUpgradeStep { ShouldRun = true }, + new TestUpgradeStep { ShouldRun = false }, + new TestUpgradeStep { ShouldRun = true }, + }; + + var sut = CreateUpgradeService(steps); + + await sut.Upgrade(); + + foreach (var step in steps) + { + Assert.AreEqual(step.ShouldRun, step.HasRun); + } + } + + [Test] + public async Task StepsRunInCollectionOrder() + { + List runOrder = new List(); + + var steps = new[] + { + new TestUpgradeStep { Id = 1 }, + new TestUpgradeStep { Id = 2 }, + new TestUpgradeStep { Id = 3 }, + }; + + // Add an method delegate that will add the step itself, that way we can know the executed order. + foreach (var step in steps) + { + step.AdditionalExecution = () => runOrder.Add(step); + } + + var sut = CreateUpgradeService(steps); + await sut.Upgrade(); + + // The ID's are strictly not necessary, but it makes potential debugging easier. + var expectedRunOrder = steps.Select(x => x.Id); + var actualRunOrder = runOrder.Select(x => x.Id); + Assert.AreEqual(expectedRunOrder, actualRunOrder); + } + + private UpgradeService CreateUpgradeService(IEnumerable steps, RuntimeLevel runtimeLevel = RuntimeLevel.Upgrade) + { + var logger = Mock.Of>(); + var stepCollection = new UpgradeStepCollection(() => steps); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(runtimeLevel); + + return new UpgradeService(stepCollection, runtimeStateMock.Object, logger); + } + + private class TestUpgradeStep : IUpgradeStep + { + public bool HasRun; + + public bool ShouldRun = true; + + public int Id; + + public Action AdditionalExecution; + + public Task ExecuteAsync() + { + HasRun = true; + + AdditionalExecution?.Invoke(); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync() => Task.FromResult(ShouldRun); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs new file mode 100644 index 0000000000..d718daba50 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.Cms.Tests.Common; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Infrastructure.Factories.Installer; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Infrastructure.Factories; + +[TestFixture] +public class DatabaseSettingsFactoryTests +{ + [Test] + public void CanBuildDatabaseSettings() + { + var metadata = CreateTestMetadata(); + var connectionString = new TestOptionsMonitor(new ConnectionStrings()); + var mapper = CreateMapper(); + + var factory = new DatabaseSettingsFactory(metadata, connectionString, mapper); + + var settingsModels = factory.GetDatabaseSettings(); + Assert.AreEqual(metadata.Count, settingsModels.Count); + AssertMapping(metadata, settingsModels); + } + + [Test] + public void IsConfiguredSetCorrectly() + { + var connectionString = new ConnectionStrings + { + ConnectionString = "SomeConnectionString", + ProviderName = "HostedTestMeta", + }; + var optionsMonitor = new TestOptionsMonitor(connectionString); + var mapper = CreateMapper(); + var metadata = CreateTestMetadata(); + + var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper); + + var settingsModels = factory.GetDatabaseSettings(); + + Assert.AreEqual(1, settingsModels.Count, "Expected only one database settings model, if a database is preconfigured we should only return the configured one."); + AssertMapping(metadata, settingsModels); + Assert.IsTrue(settingsModels.First().IsConfigured); + } + + [Test] + public void SpecifiedProviderMustExist() + { + var connectionString = new ConnectionStrings + { + ConnectionString = "SomeConnectionString", + ProviderName = "NoneExistentProvider", + }; + var optionsMonitor = new TestOptionsMonitor(connectionString); + var mapper = CreateMapper(); + var metadata = CreateTestMetadata(); + + var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper); + Assert.Throws(() => factory.GetDatabaseSettings()); + } + + /// + /// Asserts that the mapping is correct, in other words that the values in DatabaseSettingsModel is as expected. + /// + private void AssertMapping( + IEnumerable expected, + ICollection actual) + { + expected = expected.ToList(); + foreach (var model in actual) + { + var metadata = expected.FirstOrDefault(x => x.Id == model.Id); + Assert.IsNotNull(metadata); + + Assert.Multiple(() => + { + Assert.AreEqual(metadata?.SortOrder, model.SortOrder); + Assert.AreEqual(metadata.DisplayName, model.DisplayName); + Assert.AreEqual(metadata.DefaultDatabaseName, model.DefaultDatabaseName); + Assert.AreEqual(metadata.ProviderName ?? string.Empty, model.ProviderName); + Assert.AreEqual(metadata.RequiresServer, model.RequiresServer); + Assert.AreEqual(metadata.ServerPlaceholder ?? string.Empty, model.ServerPlaceholder); + Assert.AreEqual(metadata.RequiresCredentials, model.RequiresCredentials); + Assert.AreEqual(metadata.SupportsIntegratedAuthentication, model.SupportsIntegratedAuthentication); + Assert.AreEqual(metadata.RequiresConnectionTest, model.RequiresConnectionTest); + }); + } + } + + private IUmbracoMapper CreateMapper() + { + var mapper = new UmbracoMapper( + new MapDefinitionCollection(Enumerable.Empty), + Mock.Of()); + + var definition = new InstallerViewModelsMapDefinition(); + definition.DefineMaps(mapper); + return mapper; + } + + private List CreateTestMetadata() + { + + var metadata = new List + { + new TestDatabaseProviderMetadata + { + Id = Guid.Parse("EC8ACD63-8CDE-4CA5-B2A3-06322720F274"), + SortOrder = 1, + DisplayName = "FirstMetadata", + DefaultDatabaseName = "TestDatabase", + IsAvailable = true, + GenerateConnectionStringDelegate = _ => "FirstTestMetadataConnectionString", + ProviderName = "SimpleTestMeta" + }, + new TestDatabaseProviderMetadata + { + Id = Guid.Parse("C5AB4E1D-B7E4-47E5-B1A4-C9343B5F59CA"), + SortOrder = 2, + DisplayName = "SecondMetadata", + DefaultDatabaseName = "HostedTest", + IsAvailable = true, + RequiresServer = true, + ServerPlaceholder = "SomeServerPlaceholder", + RequiresCredentials = true, + RequiresConnectionTest = true, + ForceCreateDatabase = true, + GenerateConnectionStringDelegate = _ => "HostedDatabaseConnectionString", + ProviderName = "HostedTestMeta" + }, + }; + + return metadata; + } + + #nullable enable + public class TestDatabaseProviderMetadata : IDatabaseProviderMetadata + { + public Guid Id { get; set; } + + public int SortOrder { get; set; } + + public string DisplayName { get; set; } = string.Empty; + + public string DefaultDatabaseName { get; set; } = string.Empty; + + public string? ProviderName { get; set; } + + public bool SupportsQuickInstall { get; set; } + + public bool IsAvailable { get; set; } + + public bool RequiresServer { get; set; } + + public string? ServerPlaceholder { get; set; } + + public bool RequiresCredentials { get; set; } + + public bool SupportsIntegratedAuthentication { get; set; } + + public bool RequiresConnectionTest { get; set; } + + public bool ForceCreateDatabase { get; set; } + + public Func GenerateConnectionStringDelegate { get; set; } = + _ => "ConnectionString"; + + public string? GenerateConnectionString(DatabaseModel databaseModel) => GenerateConnectionStringDelegate(databaseModel); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 55d875d1bc..33f9d1884c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 10fb0bbb36..c3fc359cf5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -562,7 +562,7 @@ public class MemberControllerUnitTests var map = new MapDefinitionCollection(() => new List { - new Cms.Core.Models.Mapping.MemberMapDefinition(), + new global::Umbraco.Cms.Core.Models.Mapping.MemberMapDefinition(), memberMapDefinition, new ContentTypeMapDefinition( commonMapper, diff --git a/umbraco.sln b/umbraco.sln index 3b172779d2..e0ff14115c 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -96,6 +96,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Templates", "templa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms", "src\Umbraco.Cms\Umbraco.Cms.csproj", "{92EAA57A-CC99-4F5D-9D9C-B865293F6000}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NewBackoffice", "NewBackoffice", "{995D9EFA-8BB1-4333-80AD-C525A06FD984}" + ProjectSection(SolutionItems) = preProject + .github\New BackOffice - README.md = .github\New BackOffice - README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.ManagementApi", "src\Umbraco.Cms.ManagementApi\Umbraco.Cms.ManagementApi.csproj", "{0946531B-F06D-415B-A4E3-6CBFF5DB1C12}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Core", "src\Umbraco.New.Cms.Core\Umbraco.New.Cms.Core.csproj", "{CBCE0A1E-BF29-49A6-9581-EAB3587D823A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Infrastructure", "src\Umbraco.New.Cms.Infrastructure\Umbraco.New.Cms.Infrastructure.csproj", "{2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Web.Common", "src\Umbraco.New.Cms.Web.Common\Umbraco.New.Cms.Web.Common.csproj", "{5ED13EC6-399E-49D5-9D26-86501729B08D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{D4C3395A-BA9D-4032-9ED3-09F1FC032CBC}" ProjectSection(SolutionItems) = preProject build\csharp-docs\docfx.filter.yml = build\csharp-docs\docfx.filter.yml @@ -243,6 +255,30 @@ Global {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.Release|Any CPU.Build.0 = Release|Any CPU {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Release|Any CPU.Build.0 = Release|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Release|Any CPU.Build.0 = Release|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Release|Any CPU.Build.0 = Release|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Release|Any CPU.Build.0 = Release|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -255,6 +291,10 @@ Global {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {05C1D0C8-C592-468F-AF8F-A299B9B3A903} = {6D72A60B-0542-4AA9-A493-DD4179E838A1} + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {5ED13EC6-399E-49D5-9D26-86501729B08D} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} {D4C3395A-BA9D-4032-9ED3-09F1FC032CBC} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} {5FBDD50D-7A86-4F4D-BEB9-7967FBA4ED2C} = {D4C3395A-BA9D-4032-9ED3-09F1FC032CBC} {55B028A8-6294-46A4-BED5-7888ADB92368} = {5FBDD50D-7A86-4F4D-BEB9-7967FBA4ED2C}