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/.github/config/codeql-config.yml b/.github/config/codeql-config.yml index 77b390d392..bc9f75e77e 100644 --- a/.github/config/codeql-config.yml +++ b/.github/config/codeql-config.yml @@ -5,4 +5,5 @@ paths: paths-ignore: - '**/node_modules' - - 'src/Umbraco.Web.UI/wwwroot' \ No newline at end of file + - 'src/Umbraco.Web.UI/wwwroot' + - 'src/Umbraco.Cms.StaticAssets/wwwroot' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7bb75f0780..a3ffc10a1d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,6 +11,10 @@ jobs: CodeQL-Build: runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write steps: - name: Checkout repository @@ -20,12 +24,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: config-file: ./.github/config/codeql-config.yml - name: Setup dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '7.x' include-prerelease: true @@ -34,4 +38,4 @@ jobs: run: dotnet build umbraco.sln -c SkipTests - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 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/Server/ServerControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs new file mode 100644 index 0000000000..cdb4921ba3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Server; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/server")] +[OpenApiTag("Server")] +public abstract class ServerControllerBase : Controller +{ + +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs new file mode 100644 index 0000000000..875e685c27 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Server; + +namespace Umbraco.Cms.ManagementApi.Controllers.Server; + +[ApiVersion("1.0")] +public class StatusServerController : ServerControllerBase +{ + private readonly IRuntimeState _runtimeState; + + public StatusServerController(IRuntimeState runtimeState) => _runtimeState = runtimeState; + + [HttpGet("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ServerStatusViewModel), StatusCodes.Status200OK)] + public async Task> Get() => + new ServerStatusViewModel { ServerStatus = _runtimeState.Level }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs new file mode 100644 index 0000000000..fbd4f271e7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.ManagementApi.ViewModels.Server; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Server; + +[ApiVersion("1.0")] +public class VersionServerController : ServerControllerBase +{ + private readonly IUmbracoVersion _umbracoVersion; + + public VersionServerController(IUmbracoVersion umbracoVersion) => _umbracoVersion = umbracoVersion; + + [HttpGet("version")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(VersionViewModel), StatusCodes.Status200OK)] + public async Task> Get() => + new VersionViewModel { Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild() }; +} 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..1f57d99e4a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -0,0 +1,133 @@ +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 + // TODO this is fixed in Bjarkes PR for v10, and will need to be removed in v11 merge + GlobalSettings? globalSettings = + builder.Config.GetSection(Constants.Configuration.ConfigGlobal).Get(); + var backofficePath = (globalSettings?.UmbracoPath ?? new 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..b2ed549a52 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -0,0 +1,29 @@ + + + + net7.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.ManagementApi/ViewModels/Pagination/PagedViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs new file mode 100644 index 0000000000..7d9760bda4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +public class PagedViewModel +{ + public long Total { get; set; } + + public IEnumerable Items { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Server/ServerStatusViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Server/ServerStatusViewModel.cs new file mode 100644 index 0000000000..48cfed65c4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Server/ServerStatusViewModel.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Server; + +public class ServerStatusViewModel +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public RuntimeLevel ServerStatus { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Server/VersionViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Server/VersionViewModel.cs new file mode 100644 index 0000000000..41a55e64b7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Server/VersionViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Server; + +public class VersionViewModel +{ + public string Version { get; set; } = null!; +} 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 815be058eb..ce43dc67fc 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/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index 301795281c..4f22ac094e 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -41,17 +41,7 @@ public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty // we are cloning once, so create a new list in none mode // and deep clone all items into it var newList = new DeepCloneableList(ListCloneBehavior.None); - foreach (T item in this) - { - if (item is IDeepCloneable dc) - { - newList.Add((T)dc.DeepClone()); - } - else - { - newList.Add(item); - } - } + DeepCloneHelper.CloneListItems, T>(this, newList); return newList; case ListCloneBehavior.None: @@ -60,17 +50,7 @@ public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty case ListCloneBehavior.Always: // always clone to new list var newList2 = new DeepCloneableList(ListCloneBehavior.Always); - foreach (T item in this) - { - if (item is IDeepCloneable dc) - { - newList2.Add((T)dc.DeepClone()); - } - else - { - newList2.Add(item); - } - } + DeepCloneHelper.CloneListItems, T>(this, newList2); return newList2; default: diff --git a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs index 579716456b..baf131ca80 100644 --- a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs +++ b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Collections; @@ -7,7 +8,7 @@ namespace Umbraco.Cms.Core.Collections; /// Allows clearing all event handlers /// /// -public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged +public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged, IDeepCloneable { // need to explicitly implement with event accessor syntax in order to override in order to to clear // c# events are weird, they do not behave the same way as other c# things that are 'virtual', @@ -39,4 +40,12 @@ public class EventClearingObservableCollection : ObservableCollection event /// public void ClearCollectionChangedEvents() => _changed = null; + + public object DeepClone() + { + var clone = new EventClearingObservableCollection(); + DeepCloneHelper.CloneListItems, TValue>(this, clone); + + return clone; + } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index f0532a7203..f4f3040b79 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -157,6 +157,7 @@ public class ContentSettings internal const bool StaticHideBackOfficeLogo = false; internal const bool StaticDisableDeleteWhenReferenced = false; internal const bool StaticDisableUnpublishWhenReferenced = false; + internal const bool StaticAllowEditInvariantFromNonDefault = false; /// /// Gets or sets a value for the content notification settings. @@ -242,4 +243,10 @@ public class ContentSettings /// Get or sets the model representing the global content version cleanup policy /// public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new(); + + /// + /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. + /// + [DefaultValue(StaticAllowEditInvariantFromNonDefault)] + public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault; } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 586b3955c2..708f9b98c2 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -86,9 +86,10 @@ public class SecuritySettings [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; - /// - /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. - /// - [DefaultValue(StaticAllowEditInvariantFromNonDefault)] - public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault; + /// + /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. + /// + [Obsolete("Use ContentSettings.AllowEditFromInvariant instead")] + [DefaultValue(StaticAllowEditInvariantFromNonDefault)] + public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault; } 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/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 4c7d6490e0..31ef06c400 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; @@ -102,6 +103,20 @@ public static partial class UmbracoBuilderExtensions Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}")); + // TODO: Remove this in V12 + // This is to make the move of the AllowEditInvariantFromNonDefault setting from SecuritySettings to ContentSettings backwards compatible + // If there is a value in security settings, but no value in content setting we'll use that value, otherwise content settings always wins. + builder.Services.Configure(settings => + { + var securitySettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigSecurity}").GetValue(nameof(SecuritySettings.AllowEditInvariantFromNonDefault)); + var contentSettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigContent}").GetValue(nameof(ContentSettings.AllowEditInvariantFromNonDefault)); + + if (securitySettingsValue is not null && contentSettingsValue is null) + { + settings.AllowEditInvariantFromNonDefault = securitySettingsValue.Value; + } + }); + return builder; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 9d314526a9..d806950584 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -323,7 +323,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); + Services.AddUnique(provider => new CultureImpactFactory(provider.GetRequiredService>())); } } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml index 939b515eeb..29b242d67e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml @@ -781,8 +781,8 @@ Stiskněte Následující pro pokračování. ]]> následující, pro pokračování konfiguračního průvodce]]> Heslo výchozího uživatele musí být změněno!]]> - Výchozí uživatel byl deaktivován, nebo nemá přístup k umbracu!

Netřeba nic dalšího dělat. Klikněte na Následující pro pokračování.]]> - Heslo výchozího uživatele bylo úspěšně změněno od doby instalace!

Netřeba nic dalšího dělat. Klikněte na Následující pro pokračování.]]> + Výchozí uživatel byl deaktivován, nebo nemá přístup k umbracu!

Netřeba nic dalšího dělat. Klikněte na Následující pro pokračování.]]> + Heslo výchozího uživatele bylo úspěšně změněno od doby instalace!

Netřeba nic dalšího dělat. Klikněte na Následující pro pokračování.]]> Heslo je změněno! Mějte skvělý start, sledujte naše uváděcí videa Není nainstalováno. @@ -797,7 +797,7 @@ Vaše nastavení oprávnění může být problém!

Můžete provozovat Umbraco bez potíží, ale nebudete smět vytvářet složky a instalovat balíčky, které jsou doporučené pro plné využívání všech možností umbraca.]]>
- Vaše nastavení oprívnění není připraveno pro umbraco! + Vaše nastavení oprívnění není připraveno pro Umbraco!

Abyste mohli Umbraco provozovat, budete muset aktualizovat Vaše nastavení oprávnění.]]>
Vaše nastavení oprávnění je dokonalé!

@@ -838,7 +838,7 @@ Krok 3/5: Ověřování oprávnění k souborům Krok 4/5: Kontrola zabezpečení umbraca Krok 5/5: Umbraco je připraveno a můžete začít - Děkujeme, že jeste si vybrali umbraco + Děkujeme, že jeste si vybrali Umbraco Prohlédněte si svůj nový web Nainstalovali jste Runway, tak proč se nepodívat, jak Váš nový web vypadá.]]> Další pomoc a informace @@ -1379,7 +1379,7 @@ Makro je konfigurovatelná součást, která je skvělá pro opakovaně použitelné části návrhu, kde potřebujete předat parametry, jako jsou galerie, formuláře a seznamy. - Vložit pole stránky umbraco + Vložit pole stránky Umbraco Zobrazuje hodnotu pojmenovaného pole z aktuální stránky s možnostmi upravit hodnotu nebo alternativní hodnoty. Částečná šablona @@ -2132,7 +2132,7 @@ Profilování výkonu Umbraco aktuálně běží v režimu ladění. To znamená, že můžete použít vestavěný profiler výkonu k vyhodnocení výkonu při vykreslování stránek.

Pokud chcete aktivovat profiler pro konkrétní vykreslení stránky, jednoduše při požadavku na stránku jednoduše přidejte umbDebug=true do URL.

Pokud chcete, aby byl profiler ve výchozím nastavení aktivován pro všechna vykreslení stránky, můžete použít přepínač níže. Ve vašem prohlížeči nastaví soubor cookie, který automaticky aktivuje profiler. Jinými slovy, profiler bude ve výchozím nastavení aktivní pouze ve vašem prohlížeči, ne v ostatních.

+

Umbraco aktuálně běží v režimu ladění. To znamená, že můžete použít vestavěný profiler výkonu k vyhodnocení výkonu při vykreslování stránek.

Pokud chcete aktivovat profiler pro konkrétní vykreslení stránky, jednoduše při požadavku na stránku jednoduše přidejte umbDebug=true do URL.

Pokud chcete, aby byl profiler ve výchozím nastavení aktivován pro všechna vykreslení stránky, můžete použít přepínač níže. Ve vašem prohlížeči nastaví soubor cookie, který automaticky aktivuje profiler. Jinými slovy, profiler bude ve výchozím nastavení aktivní pouze ve vašem prohlížeči, ne v ostatních.

]]>
Ve výchozím stavu aktivovat profiler diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index d0c6a45d27..e34cc2ab29 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -449,13 +449,13 @@ Gweinyddu enwau gwesteia Cau'r ffenestr yma Ydych chi'n sicr eich bod eisiau dileu - %0% yn seiliedig ar %1%]]> + %0% yn seiliedig ar %1%]]> Ydych chi'n sicr eich bod eisiau analluogi Wyt ti'n siŵr fod ti eisiau dileu - %0%]]> - %0%]]> + %0%]]> + %0%]]> Ydych chi'n sicr? Ydych chi'n sicr? @@ -546,8 +546,8 @@ Dewiswch ffurfweddiad Dewiswch damaid Bydd hyn yn dileu'r nod a'i holl ieithoedd. Os mai dim ond un iaith yr ydych am ei dileu, ewch i'w anghyhoedd yn lle. - %0%.]]> - %0% o'r grŵp %1%]]> + %0%.]]> + %0% o'r grŵp %1%]]> Ydw, dileu @@ -965,7 +965,7 @@ nesaf i barhau gyda'r dewin ffurfwedd]]> Mae angen newid cyfrinair y defnyddiwr Diofyn!]]> - Mae'r defnyddiwr Diofyn wedi'u analluogi neu does dim hawliau i Umbraco!

Does dim angen unrhyw weithredoedd pellach. Cliciwch Nesaf i barhau.]]> + Mae'r defnyddiwr Diofyn wedi'u analluogi neu does dim hawliau i Umbraco!

Does dim angen unrhyw weithredoedd pellach. Cliciwch Nesaf i barhau.]]> Mae cyfrinair y defnyddiwr Diofyn wedi'i newid yn llwyddiannus ers y gosodiad!

Does dim angen unrhyw weithredoedd pellach. Cliciwch Nesaf i barhau.]]> Mae'r cyfrinair wedi'i newid! Cewch gychwyn gwych, gwyliwch ein fideos rhaglith @@ -2695,12 +2695,12 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Mae Umbraco yn rhedeg mewn modd dadfygio. Mae hyn yn golygu y gallwch chi ddefnyddio'r proffiliwr perfformiad adeiledig i asesu'r perfformiad wrth rendro tudalennau.

- OS ti eisiau actifadu'r proffiliwr am rendro tudalen penodol, bydd angen ychwanegu umbDebug=true i'r ymholiad wrth geisio am y tudalen + OS ti eisiau actifadu'r proffiliwr am rendro tudalen penodol, bydd angen ychwanegu umbDebug=true i'r ymholiad wrth geisio am y tudalen

Os ydych chi am i'r proffiliwr gael ei actifadu yn ddiofyn am bob rendrad tudalen, gallwch chi ddefnyddio'r togl isod. Bydd e'n gosod cwci yn eich porwr, sydd wedyn yn actifadu'r proffiliwr yn awtomatig. - Mewn geiriau eraill, bydd y proffiliwr dim ond yn actif yn ddiofyn yn eich porwr chi - nid porwr pawb eraill. + Mewn geiriau eraill, bydd y proffiliwr dim ond yn actif yn ddiofyn yn eich porwr chi - nid porwr pawb eraill.

]]>
@@ -2709,7 +2709,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang - Ni ddylech chi fyth adael i safle cynhyrchu redeg yn y modd dadfygio. Mae'r modd dadfygio yn gallu cael ei diffodd trwy ychwanegu'r gosodiad debug="false" ar yr elfen <grynhoi /> yn web.config. + Ni ddylech chi fyth adael i safle cynhyrchu redeg yn y modd dadfygio. Mae'r modd dadfygio yn gallu cael ei diffodd trwy ychwanegu'r gosodiad debug="false" ar yr elfen <grynhoi /> yn web.config.

]]>
@@ -2719,7 +2719,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Mae Umbraco ddim yn rhedeg mewn modd dadfygio ar hyn o bryd, felly nid allwch chi ddefnyddio'r proffiliwer adeiledig. Dyma sut y dylai fod ar gyfer safle cynhyrchu.

- Mae'r modd dadfygio yn gallu cael ei throi arno gan ychwanegu'r gosodiad debug="true" ar yr elfen <grynhoi /> yn web.config. + Mae'r modd dadfygio yn gallu cael ei throi arno gan ychwanegu'r gosodiad debug="true" ar yr elfen <grynhoi /> yn web.config.

]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index b03fa9d884..93b0f95af2 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -465,7 +465,7 @@ Er du sikker på at du vil slette Er du sikker på du vil deaktivere Er du sikker på at du vil fjerne - %0%]]> + %0%]]> Er du sikker på at du vil forlade Umbraco? Er du sikker? Klip @@ -556,8 +556,8 @@ Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. - %0%]]> - %0% fra gruppen]]> + %0%]]> + %0% fra %1% gruppen]]> Ja, fjern diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index e6ba39eb17..0a8b03b115 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -469,10 +469,10 @@ Manage hostnames Close this window Are you sure you want to delete - %0% of %1% items]]> + %0% of %1% items]]> Are you sure you want to disable Are you sure you want to remove - %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -564,8 +564,8 @@ This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - %0%.]]> - %0% from the %1% group]]> + %0%.]]> + %0% from the %1% group]]> Yes, remove You are deleting the layout Modifying layout will result in loss of data for any existing content that is based on this configuration. @@ -948,7 +948,7 @@ The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> The password is changed! @@ -1873,7 +1873,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Keep all versions newer than days Keep latest version per day for days Prevent cleanup - NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> + NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> Add language @@ -2611,12 +2611,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.

- If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page.

If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. - In other words, the profiler will only be active by default in your browser - not everyone else's. + In other words, the profiler will only be active by default in your browser - not everyone else's.

]]>
@@ -2625,7 +2625,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont - You should never let a production site run in debug mode. Debug mode is turned off by setting Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment variable. + You should never let a production site run in debug mode. Debug mode is turned off by setting Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment variable.

]]>
@@ -2635,7 +2635,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.

- Debug mode is turned on by setting Umbraco:CMS:Hosting:Debug to true in appsettings.json, appsettings.{Environment}.json or via an environment variable. + Debug mode is turned on by setting Umbraco:CMS:Hosting:Debug to true in appsettings.json, appsettings.{Environment}.json or via an environment variable.

]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 70aa1c2d5c..88030198a3 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -483,10 +483,10 @@ Name Close this window Are you sure you want to delete - %0% of %1% items]]> + %0% of %1% items]]> Are you sure you want to disable Are you sure you want to remove - %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -579,8 +579,8 @@ This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - %0%.]]> - %0% from the %1% group]]> + %0%.]]> + %0% from the %1% group]]> Yes, remove You are deleting the layout Modifying layout will result in loss of data for any existing content that is based on this configuration. @@ -975,7 +975,7 @@ The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> The password is changed! @@ -1947,7 +1947,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Keep all versions newer than days Keep latest version per day for days Prevent cleanup - NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> + NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> Changing a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json. @@ -1967,7 +1967,11 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Fall back language none - %0% is shared across all languages.]]> + %0% is shared across languages and segments.]]> + %0% is shared across all languages.]]> + %0% is shared across all segments.]]> + Shared: Languages + Shared: Segments Add parameter @@ -2713,12 +2717,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.

- If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page.

If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. - In other words, the profiler will only be active by default in your browser - not everyone else's. + In other words, the profiler will only be active by default in your browser - not everyone else's.

]]>
@@ -2727,7 +2731,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont - You should never let a production site run in debug mode. Debug mode is turned off by setting Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment variable. + You should never let a production site run in debug mode. Debug mode is turned off by setting Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment variable.

]]>
@@ -2737,7 +2741,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.

- Debug mode is turned on by setting Umbraco:CMS:Hosting:Debug to true in appsettings.json, appsettings.{Environment}.json or via an environment variable. + Debug mode is turned on by setting Umbraco:CMS:Hosting:Debug to true in appsettings.json, appsettings.{Environment}.json or via an environment variable.

]]> @@ -2906,22 +2910,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Aggregate data will be shared on a regular basis as well as learnings from these metrics.
Hopefully, you will help us collect some valuable data.
-
We WILL NOT collect any personal data such as content, code, user information, and all data will be fully anonymized. +
We WILL NOT collect any personal data such as content, code, user information, and all data will be fully anonymized. ]]> We will only send an anonymized site ID to let us know that the site exists. - We will send an anonymized site ID, umbraco version, and packages installed + We will send an anonymized site ID, Umbraco version, and packages installed -
  • Anonymized site ID, umbraco version, and packages installed.
  • +
  • Anonymized site ID, Umbraco version, and packages installed.
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
  • - We might change what we send on the Detailed level in the future. If so, it will be listed above. -
    By choosing "Detailed" you agree to current and future anonymized information being collected.
    + We might change what we send on the Detailed level in the future. If so, it will be listed above. +
    By choosing "Detailed" you agree to current and future anonymized information being collected.
    ]]>
    diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 2c64f3f9a6..3e59088ef9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -601,7 +601,7 @@ Pincha en Próximo para continuar. ]]> próximo para continuar con el asistente de configuración]]> La contraseña del usuario por defecto debe ser cambiada]]> - El usuario por defecto ha sido deshabilitado o ha perdido el acceso a Umbraco!

    Pincha en Próximo para continuar.]]> + El usuario por defecto ha sido deshabilitado o ha perdido el acceso a Umbraco!

    Pincha en Próximo para continuar.]]> ¡La contraseña del usuario por defecto ha sido cambiada desde que se instaló!

    No hay que realizar ninguna tarea más. Pulsa Siguiente para proseguir.]]> ¡La contraseña se ha cambiado! Ten un buen comienzo, visita nuestros videos de introducción diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml index 9013a4473f..24d5f565e5 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml @@ -390,7 +390,7 @@ Nom Fermer cette fenêtre Êtes-vous certain(e) de vouloir supprimer - %0% des %1% éléments]]> + %0% des %1% éléments]]> Êtes-vous certain(e) de vouloir désactiver Êtes-vous certain(e)? Êtes-vous certain(e)? @@ -799,7 +799,7 @@ poursuivre. ]]> Suivant pour poursuivre la configuration]]> Le mot de passe par défaut doit être modifié !]]> - L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

    Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> + L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

    Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> Le mot de passe par défaut a été modifié avec succès depuis l'installation!

    Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> Le mot de passe a été modifié ! Pour bien commencer, regardez nos vidéos d'introduction @@ -2177,12 +2177,12 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Umbraco est actuellement exécuté en mode debug. Cela signifie que vous pouvez utiliser le profileur de performances intégré pour évaluer les performance lors du rendu des pages.

    - Si vous souhaitez activer le profileur pour le rendu d'une page spécifique, ajoutez simplement umbDebug=true au querystring lorsque vous demandez la page. + Si vous souhaitez activer le profileur pour le rendu d'une page spécifique, ajoutez simplement umbDebug=true au querystring lorsque vous demandez la page.

    Si vous souhaitez que le profileur soit activé par défaut pour tous les rendus de pages, vous pouvez utiliser le bouton bascule ci-dessous. Cela créera un cookie dans votre browser, qui activera alors le profileur automatiquement. - En d'autres termes, le profileur ne sera activé par défaut que dans votre browser - pas celui des autres. + En d'autres termes, le profileur ne sera activé par défaut que dans votre browser - pas celui des autres.

    ]]>
    diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml index 0996c81ba0..c52961307d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml @@ -366,7 +366,7 @@ proceed. ]]> next to continue the configuration wizard]]> The Default users’ password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

    No further actions needs to be taken. Click Next to proceed.]]> + The Default user has been disabled or has no access to Umbraco!

    No further actions needs to be taken. Click Next to proceed.]]> The Default user's password has been successfully changed since the installation!

    No further actions needs to be taken. Click Next to proceed.]]> הסיסמה שונתה! התחל מכאן, צפה בסרטוני ההדרכה עבור אומברקו diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml index 23bff095a3..cea82fc4e0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml @@ -487,8 +487,8 @@ Sei sicuro di voler eliminare Sei sicuro di voler disabilitare Sei sicuro di voler rimuovere - %0%]]> - %0%]]> + %0%]]> + %0%]]> Taglia @@ -574,8 +574,8 @@ Seleziona snippet - %0%.]]> - %0% dal gruppo %1%]]> + %0%.]]> + %0% dal gruppo %1%]]> Si, rimuovi @@ -2767,12 +2767,12 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in Umbraco attualmente funziona in modalità debug. Ciò significa che puoi utilizzare il profiler delle prestazioni integrato per valutare le prestazioni durante il rendering delle pagine.

    - Se vuoi attivare il profiler per il rendering di una pagina specifica, aggiungi semplicemente umbDebug=true alla querystring quando richiedi la pagina. + Se vuoi attivare il profiler per il rendering di una pagina specifica, aggiungi semplicemente umbDebug=true alla querystring quando richiedi la pagina.

    Se vuoi che il profiler sia attivato per impostazione predefinita per tutti i rendering di pagina, puoi utilizzare l'interruttore qui sotto. Verrà impostato un cookie nel tuo browser, che quindi attiverà automaticamente il profiler. - In altre parole, il profiler sarà attivo per impostazione predefinita solo nel tuo browser, non in quello di tutti gli altri. + In altre parole, il profiler sarà attivo per impostazione predefinita solo nel tuo browser, non in quello di tutti gli altri.

    ]]>
    @@ -2781,7 +2781,7 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in - Non dovresti mai lasciare che un sito di produzione venga eseguito in modalità debug. La modalità di debug viene disattivata impostando debug="false" nell'elemento <compilation /> nel file web.config. + Non dovresti mai lasciare che un sito di produzione venga eseguito in modalità debug. La modalità di debug viene disattivata impostando debug="false" nell'elemento <compilation /> nel file web.config.

    ]]>
    @@ -2791,7 +2791,7 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in Umbraco attualmente non viene eseguito in modalità debug, quindi non è possibile utilizzare il profiler integrato. Questo è come dovrebbe essere per un sito produttivo.

    - La modalità di debug viene attivata impostando debug="true" nell'elemento <compilation /> in web.config. + La modalità di debug viene attivata impostando debug="true" nell'elemento <compilation /> in web.config.

    ]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml index 2d4c80570b..bcf9e8c9a9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml @@ -481,7 +481,7 @@ を押して続行してください。]]> 次へ をクリックして設定ウィザードを進めてください。]]> デフォルトユーザーのパスワードを変更する必要があります!]]> - デフォルトユーザーは無効化されているかUmbracoにアクセスできない状態になっています!

    これ以上のアクションは必要ありません。次へをクリックして続行してください。]]> + デフォルトユーザーは無効化されているかUmbracoにアクセスできない状態になっています!

    これ以上のアクションは必要ありません。次へをクリックして続行してください。]]> インストール後にデフォルトユーザーのパスワードが変更されています!

    これ以上のアクションは必要ありません。次へをクリックして続行してください。]]> パスワードは変更されました! 始めに、ビデオによる解説を見ましょう @@ -555,7 +555,7 @@ Runwayをインストールして作られた新しいウェブサイトがど Umbraco Version 3 Umbraco Version 4 見る - umbraco %0% の新規インストールまたは3.0からの更新について設定方法を案内します。 + Umbraco %0% の新規インストールまたは3.0からの更新について設定方法を案内します。

    "次へ"を押してウィザードを開始します。]]>
    @@ -849,9 +849,9 @@ Runwayをインストールして作られた新しいウェブサイトがど コンテンツ領域プレースホルダーの挿入 ディクショナリ アイテムを挿入 マクロの挿入 - umbraco ページフィールドの挿入 + Umbraco ページフィールドの挿入 マスターテンプレート - umbraco テンプレートタグのクイックガイド + Umbraco テンプレートタグのクイックガイド テンプレート diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml index 852d8765aa..6a20975bb1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml @@ -357,7 +357,7 @@ 계속 진행하시려면 다음 을 누르세요. ]]> 다음을 클릭하시면 설정마법사를 계속 진행합니다.]]> 기본 사용자의 암호가 변경되어야 합니다!]]> - 기본 사용자가 비활성화되었거나 Umbraco에 접근할 수 없습니다!

    더 이상 과정이 필요없으시면 다음을 눌러주세요.]]> + 기본 사용자가 비활성화되었거나 Umbraco에 접근할 수 없습니다!

    더 이상 과정이 필요없으시면 다음을 눌러주세요.]]> 설치후 기본사용자의 암호가 성공적으로 변경되었습니다!

    더 이상 과정이 필요없으시면 다음을 눌러주세요.]]> 비밀번호가 변경되었습니다! 편리한 시작을 위해, 소개 Video를 시청하세요 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml index 30d2da3e4f..87bcb3138a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml @@ -420,7 +420,7 @@ Trykk Neste for å fortsette.]]> neste for å fortsette konfigurasjonsveiviseren]]> Passordet til standardbrukeren må endres!]]> - Standardbrukeren har blitt deaktivert eller har ingen tilgang til Umbraco!

    Ingen videre handling er nødvendig. Klikk neste for å fortsette.]]> + Standardbrukeren har blitt deaktivert eller har ingen tilgang til Umbraco!

    Ingen videre handling er nødvendig. Klikk neste for å fortsette.]]> Passordet til standardbrukeren har blitt forandret etter installasjonen!

    Ingen videre handling er nødvendig. Klikk Neste for å fortsette.]]> Passordet er blitt endret! Få en god start med våre introduksjonsvideoer diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 163fd14199..28793081b7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -439,7 +439,7 @@ Weet je zeker dat je dit wilt verwijderen Weet je zeker dat je dit wilt uitschakelen Weet u zeker dat u wilt verwijderen - %0% wil verwijderen]]> + %0% wil verwijderen]]> Weet je het zeker? Weet je het zeker? Knippen @@ -529,8 +529,8 @@ Dit zal de node en al zijn talen verwijderen. Als je slechts één taal wil verwijderen, moet je de node in die taal depubliceren. - %0% verwijderen.]]> - %0% verwijderen van de %1% groep]]> + %0% verwijderen.]]> + %0% verwijderen van de %1% groep]]> Ja, verwijderen @@ -889,7 +889,7 @@ Het wachtwoord van de default gebruiker dient veranderd te worden!]]> - De default gebruiker is geblokkeerd of heeft geen toegang tot Umbraco!

    Geen verdere actie noodzakelijk. Klik Volgende om verder te gaan.]]> + De default gebruiker is geblokkeerd of heeft geen toegang tot Umbraco!

    Geen verdere actie noodzakelijk. Klik Volgende om verder te gaan.]]> Het wachtwoord van de default gebruiker is sinds installatie met succes veranderd.

    Geen verdere actie noodzakelijk. Klik Volgende om verder te gaan.]]> Het wachtwoord is veranderd! @@ -2399,12 +2399,12 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Umbraco wordt uitgevoerd in de foutopsporingsmodus. Dit betekent dat u de ingebouwde prestatieprofiler kunt gebruiken om de prestaties te beoordelen bij het renderen van pagina's.

    - Als je de profiler voor een specifieke paginaweergave wilt activeren, voeg je umbDebug=true toe aan de querystring wanneer je de pagina opvraagt. + Als je de profiler voor een specifieke paginaweergave wilt activeren, voeg je umbDebug=true toe aan de querystring wanneer je de pagina opvraagt.

    Als je wil dat de profiler standaard wordt geactiveerd voor alle paginaweergaven, kun je de onderstaande schakelaar gebruiken. Het plaatst een cookie in je browser, die vervolgens de profiler automatisch activeert. - Met andere woorden, de profiler zal alleen voor jouw browser actief zijn, niet voor andere bezoekers. + Met andere woorden, de profiler zal alleen voor jouw browser actief zijn, niet voor andere bezoekers.

    ]]>
    diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml index 39d0cfc4a1..25060a4bd3 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml @@ -358,7 +358,7 @@ Pressione Próximo para prosseguir.]]> próximo para continuar com o assistente de configuração]]> A senha do usuário padrão precisa ser alterada!]]> - O usuário padrão foi desabilitado ou não tem acesso à Umbraco!

    Nenhuma ação posterior precisa ser tomada. Clique Próximo para prosseguir.]]> + O usuário padrão foi desabilitado ou não tem acesso à Umbraco!

    Nenhuma ação posterior precisa ser tomada. Clique Próximo para prosseguir.]]> A senha do usuário padrão foi alterada com sucesso desde a instalação!

    Nenhuma ação posterior é necessária. Clique Próximo para prosseguir.]]> Senha foi alterada! Comece com o pé direito, assista nossos vídeos introdutórios diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml index af3f157bf4..fa359fbbbc 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml @@ -493,7 +493,7 @@ Tryck Nästa för att fortsätta.]]> Nästa för att fortsätta med konfigurationsguiden]]> Lösenordet på standardanvändaren måste bytas!]]> - Standardanvändaren har avaktiverats eller har inte åtkomst till Umbraco!

    Du behöver inte göra något ytterligare här. Klicka Next för att fortsätta.]]> + Standardanvändaren har avaktiverats eller har inte åtkomst till Umbraco!

    Du behöver inte göra något ytterligare här. Klicka Next för att fortsätta.]]> Standardanvändarens lösenord har ändrats sedan installationen!

    Du behöver inte göra något ytterligare här. Klicka Nästa för att fortsätta.]]> Lösenordet är ändrat! Få en flygande start, kolla på våra introduktionsvideor diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml index 3ef3db0ad6..47549f5f40 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml @@ -393,7 +393,7 @@ Silmek istediğinizden emin misiniz Devre dışı bırakmak istediğinizden emin misiniz Kaldırmak istediğinizden emin misiniz - %0% kullanımını kaldırmak istediğinizden emin misiniz?]]> + %0% kullanımını kaldırmak istediğinizden emin misiniz?]]> Emin misiniz? Emin misiniz? Kes @@ -471,8 +471,8 @@ Düzenleyici seçin Snippet seçin Bu, düğümü ve tüm dillerini silecektir. Yalnızca bir dili silmek istiyorsanız, bunun yerine düğümü o dilde yayından kaldırmalısınız. - %0% kullanıcısını kaldıracaktır.]]> - %0% kullanıcısını %1% grubundan kaldıracak]]> + %0% kullanıcısını kaldıracaktır.]]> + %0% kullanıcısını %1% grubundan kaldıracak]]> Evet, kaldır @@ -818,7 +818,7 @@ ileri 'yi tıklayın]]> Varsayılan kullanıcıların şifresinin değiştirilmesi gerekiyor! ]]> - Varsayılan kullanıcı devre dışı bırakıldı veya Umbraco'ya erişimi yok!

    Başka işlem yapılmasına gerek yok. Devam etmek için İleri 'yi tıklayın.]]> + Varsayılan kullanıcı devre dışı bırakıldı veya Umbraco'ya erişimi yok!

    Başka işlem yapılmasına gerek yok. Devam etmek için İleri 'yi tıklayın.]]> Varsayılan kullanıcının şifresi kurulumdan bu yana başarıyla değiştirildi!

    Başka işlem yapılmasına gerek yok. Devam etmek için İleri 'yi tıklayın.]]> Şifre değiştirildi! Harika bir başlangıç ​​yapın, tanıtım videolarımızı izleyin @@ -2289,12 +2289,12 @@ Web sitenizi yönetmek için, Umbraco'nun arka ofisini açın ve içerik eklemey Umbraco şu anda hata ayıklama modunda çalışıyor. Bu, sayfaları işlerken performansı değerlendirmek için yerleşik performans profilleyicisini kullanabileceğiniz anlamına gelir.

    - Profil oluşturucuyu belirli bir sayfa oluşturma için etkinleştirmek istiyorsanız, sayfayı talep ederken sorgu dizesine umbDebug=true eklemeniz yeterlidir. + Profil oluşturucuyu belirli bir sayfa oluşturma için etkinleştirmek istiyorsanız, sayfayı talep ederken sorgu dizesine umbDebug=true eklemeniz yeterlidir.

    Profilcinin tüm sayfa görüntülemeleri için varsayılan olarak etkinleştirilmesini istiyorsanız, aşağıdaki geçişi kullanabilirsiniz. Tarayıcınızda, profil oluşturucuyu otomatik olarak etkinleştiren bir çerez ayarlayacaktır. - Başka bir deyişle, profil oluşturucu yalnızca tarayıcınızda varsayılan olarak etkin olacaktır - diğer herkesin değil. + Başka bir deyişle, profil oluşturucu yalnızca tarayıcınızda varsayılan olarak etkin olacaktır - diğer herkesin değil.

    ]]>
    diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml index d8132c151b..5d9a6e9ab3 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml @@ -501,7 +501,7 @@ 点击下一步继续。]]> 下一步继续]]> 需要修改默认密码!]]> - 默认账户已禁用或无权访问系统!

    点击下一步继续。]]> + 默认账户已禁用或无权访问系统!

    点击下一步继续。]]> 安装过程中默认用户密码已更改

    点击下一步继续。]]> 密码已更改 作为入门者,从视频教程开始吧! diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml index 216dc3d0fe..b3e3b7bdcf 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml @@ -493,8 +493,8 @@ 點選下一步繼續。]]> 下一步繼續設定精靈。]]> 預設使用者的密碼必須更改!]]> - 預設使用者已經被暫停或沒有Umbraco的使用權!

    不需更多的操作步驟。點選下一步繼續。]]> - 安裝後預設使用者的密碼已經成功修改!

    不需更多的操作步驟。點選下一步繼續。]]> + 預設使用者已經被暫停或沒有Umbraco的使用權!

    不需更多的操作步驟。點選下一步繼續。]]> + 安裝後預設使用者的密碼已經成功修改!

    不需更多的操作步驟。點選下一步繼續。]]> 密碼已更改 作為入門者,從視頻教程開始吧! 安裝失敗。 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 c8962c5fb9..17b89d8ec0 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.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index e9fcc61e7c..8ecf0bfc8f 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -60,15 +60,8 @@ public abstract class ContentBase : TreeEntityBase, IContentBase _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); - - // track all property types on this content type, these can never change during the lifetime of this single instance - // there is no real extra memory overhead of doing this since these property types are already cached on this object via the - // properties already. - AllPropertyTypes = new List(contentType.CompositionPropertyTypes); } - internal IReadOnlyList AllPropertyTypes { get; } - [IgnoreDataMember] public ISimpleContentType ContentType { get; private set; } @@ -146,7 +139,6 @@ public abstract class ContentBase : TreeEntityBase, IContentBase base.PerformDeepClone(clone); var clonedContent = (ContentBase)clone; - // Need to manually clone this since it's not settable clonedContent.ContentType = ContentType; diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs index d0f2b9aed6..9368de8ce1 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs @@ -15,6 +15,9 @@ public class ContentPropertyDisplay : ContentPropertyBasic Validation = new PropertyTypeValidation(); } + [DataMember(Name = "variations")] + public ContentVariation Variations { get; set; } + [DataMember(Name = "label", IsRequired = true)] [Required] public string? Label { get; set; } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs index 90dd6ce5c9..0ba344f7fc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs @@ -42,6 +42,9 @@ public class ContentTypeBasic : EntityBasic [DataMember(Name = "thumbnail")] public string? Thumbnail { get; set; } + [DataMember(Name = "variations")] + public ContentVariation Variations { get; set; } + /// /// Returns true if the icon represents a CSS class instead of a file path /// diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs index 3c292a7e6a..110ab98547 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs @@ -16,6 +16,9 @@ public class DocumentTypeDisplay : ContentTypeCompositionDisplay AllowedTemplates { get; set; } + [DataMember(Name = "variations")] + public ContentVariation Variations { get; set; } + [DataMember(Name = "defaultTemplate")] public EntityBasic? DefaultTemplate { get; set; } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index ce34dab6f1..7b9110f432 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -212,4 +212,16 @@ public static class DeepCloneHelper public bool IsList => GenericListType != null; } + + public static void CloneListItems(TList source, TList target) + where TList : ICollection + { + target.Clear(); + foreach (TEntity entity in source) + { + target.Add(entity is IDeepCloneable deepCloneableEntity + ? (TEntity)deepCloneableEntity.DeepClone() + : entity); + } + } } diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index 887477c743..18bc984853 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -85,6 +85,8 @@ public abstract class BeingDirtyBase : IRememberBeingDirty /// public event PropertyChangedEventHandler? PropertyChanged; + protected void ClearPropertyChangedEvents() => PropertyChanged = null; + /// /// Registers that a property has changed. /// diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs index eb6c6d92e0..22407219eb 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -56,6 +56,9 @@ internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper c.SortOrder).Select(x => x.Id.Value); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); + target.Variations = source.Variations; } // no MapAll - relies on the non-generic method @@ -754,6 +757,7 @@ public class ContentTypeMapDefinition : IMapDefinition : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); target.Trashed = source.Trashed; target.Udi = source.Udi; + target.Variations = source.Variations; } // no MapAll - relies on the non-generic method diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs index 5441320b0f..91bd8c3589 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs @@ -20,7 +20,7 @@ public class ContentVariantMapper private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IContentService _contentService; private readonly IUserService _userService; - private SecuritySettings _securitySettings; + private ContentSettings _contentSettings; public ContentVariantMapper( ILocalizationService localizationService, @@ -28,17 +28,36 @@ public class ContentVariantMapper IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IContentService contentService, IUserService userService, - IOptionsMonitor securitySettings) + IOptionsMonitor contentSettings) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _contentService = contentService; _userService = userService; - _securitySettings = securitySettings.CurrentValue; - securitySettings.OnChange(settings => _securitySettings = settings); + _contentSettings = contentSettings.CurrentValue; + contentSettings.OnChange(settings => _contentSettings = settings); } + [Obsolete("Use constructor that takes all parameters instead")] + public ContentVariantMapper( + ILocalizationService localizationService, + ILocalizedTextService localizedTextService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentService contentService, + IUserService userService, + IOptionsMonitor securitySettings) + : this( + localizationService, + localizedTextService, + backOfficeSecurityAccessor, + contentService, + userService, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + [Obsolete("Use constructor that takes all parameters instead")] public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) : this( localizationService, @@ -244,7 +263,7 @@ public class ContentVariantMapper if (variantDisplay.Language is null) { var defaultLanguageId = _localizationService.GetDefaultLanguageId(); - if (_securitySettings.AllowEditInvariantFromNonDefault || (defaultLanguageId.HasValue && group.HasAccessToLanguage(defaultLanguageId.Value))) + if (_contentSettings.AllowEditInvariantFromNonDefault || (defaultLanguageId.HasValue && group.HasAccessToLanguage(defaultLanguageId.Value))) { hasAccess = true; } diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index 5e92783d14..3c79d1c12f 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Models.Mapping; diff --git a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs index 70d4826ab6..c8637c3042 100644 --- a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs @@ -26,24 +26,12 @@ public static class MapperContextExtensions /// /// Sets a context culture. /// - public static void SetCulture(this MapperContext context, string? culture) - { - if (culture is not null) - { - context.Items[CultureKey] = culture; - } - } + public static void SetCulture(this MapperContext context, string? culture) => context.Items[CultureKey] = culture; /// /// Sets a context segment. /// - public static void SetSegment(this MapperContext context, string? segment) - { - if (segment is not null) - { - context.Items[SegmentKey] = segment; - } - } + public static void SetSegment(this MapperContext context, string? segment) => context.Items[SegmentKey] = segment; /// /// Get included properties. diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 0699ecbc0d..8fe28f7751 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -283,12 +283,7 @@ public class PropertyType : EntityBase, IPropertyType, IEquatable base.PerformDeepClone(clone); var clonedEntity = (PropertyType)clone; - - // need to manually assign the Lazy value as it will not be automatically mapped - if (PropertyGroupId != null) - { - clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); - } + clonedEntity.ClearPropertyChangedEvents(); } /// diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 8789ef5052..fdf3761366 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Models; public class PublicAccessEntry : EntityBase { private readonly List _removedRules = new(); - private readonly EventClearingObservableCollection _ruleCollection; + private EventClearingObservableCollection _ruleCollection; private int _loginNodeId; private int _noAccessNodeId; private int _protectedNodeId; @@ -144,11 +144,13 @@ public class PublicAccessEntry : EntityBase var cloneEntity = (PublicAccessEntry)clone; - if (cloneEntity._ruleCollection != null) - { - cloneEntity._ruleCollection.ClearCollectionChangedEvents(); // clear this event handler if any - cloneEntity._ruleCollection.CollectionChanged += - cloneEntity.RuleCollection_CollectionChanged; // re-assign correct event handler - } + // clear this event handler if any + cloneEntity._ruleCollection.ClearCollectionChangedEvents(); + + // clone the rule collection explicitly + cloneEntity._ruleCollection = (EventClearingObservableCollection)_ruleCollection.DeepClone(); + + // re-assign correct event handler + cloneEntity._ruleCollection.CollectionChanged += cloneEntity.RuleCollection_CollectionChanged; } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs index 6cd7645868..635b590ba4 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs @@ -21,7 +21,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Content Picker", "contentpicker", ValueType = ValueTypes.String, - Group = Constants.PropertyEditors.Groups.Pickers)] + Group = Constants.PropertyEditors.Groups.Pickers, + ValueEditorIsReusable = true)] public class ContentPickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index b2b95f475b..3009e8af62 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -20,6 +20,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; [DataContract] public class DataEditor : IDataEditor { + private readonly bool _canReuseValueEditor; + private IDataValueEditor? _reusableValueEditor; private IDictionary? _defaultConfiguration; /// @@ -48,6 +50,8 @@ public class DataEditor : IDataEditor Icon = Attribute.Icon; Group = Attribute.Group; IsDeprecated = Attribute.IsDeprecated; + + _canReuseValueEditor = Attribute.ValueEditorIsReusable; } /// @@ -118,18 +122,14 @@ public class DataEditor : IDataEditor /// instance is returned. Otherwise, a new instance is created by CreateValueEditor. /// /// - /// The instance created by CreateValueEditor is not cached, i.e. - /// a new instance is created each time the property value is retrieved. The - /// property editor is a singleton, and the value editor cannot be a singleton - /// since it depends on the datatype configuration. - /// - /// - /// Technically, it could be cached by datatype but let's keep things - /// simple enough for now. + /// The instance created by CreateValueEditor is cached if allowed by the DataEditor + /// attribute ( == true). /// /// - // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor + ?? (_canReuseValueEditor + ? _reusableValueEditor ??= CreateValueEditor() + : CreateValueEditor()); /// /// diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index ce15c66a80..d9164d07ab 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -178,4 +178,10 @@ public sealed class DataEditorAttribute : Attribute /// /// A deprecated editor is still supported but not proposed in the UI. public bool IsDeprecated { get; set; } + + /// + /// Gets or sets a value indicating whether the value editor can be reused (cached). + /// + /// While most value editors can be reused, complex editors (e.g. block based editors) might not be applicable for reuse. + public bool ValueEditorIsReusable { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs index a936a72512..a6fa0633d7 100644 --- a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs @@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; EditorType.PropertyValue | EditorType.MacroParameter, "Decimal", "decimal", - ValueType = ValueTypes.Decimal)] + ValueType = ValueTypes.Decimal, + ValueEditorIsReusable = true)] public class DecimalPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs index 12b1b2c8ef..c9e8545b68 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs @@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Eye Dropper Color Picker", "eyedropper", Icon = "icon-colorpicker", - Group = Constants.PropertyEditors.Groups.Pickers)] + Group = Constants.PropertyEditors.Groups.Pickers, + ValueEditorIsReusable = true)] public class EyeDropperColorPickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs index a504c7df31..6910912c51 100644 --- a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs @@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; EditorType.PropertyValue | EditorType.MacroParameter, "Numeric", "integer", - ValueType = ValueTypes.Integer)] + ValueType = ValueTypes.Integer, + ValueEditorIsReusable = true)] public class IntegerPropertyEditor : DataEditor { public IntegerPropertyEditor( diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index ae2f4c0897..eb4c96552f 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; Constants.PropertyEditors.Aliases.Label, "Label", "readonlyvalue", - Icon = "icon-readonly")] + Icon = "icon-readonly", + ValueEditorIsReusable = true)] public class LabelPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs index aa6e881aa2..4bc17c8cfc 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "markdowneditor", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.RichContent, - Icon = "icon-code")] + Icon = "icon-code", + ValueEditorIsReusable = true)] public class MarkdownPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs index e839c0b527..dcb19624be 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "membergrouppicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.MemberGroup)] + Icon = Constants.Icons.MemberGroup, + ValueEditorIsReusable = true)] public class MemberGroupPickerPropertyEditor : DataEditor { public MemberGroupPickerPropertyEditor( diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs index 241736737e..b16acaffb1 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs @@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "memberpicker", ValueType = ValueTypes.String, Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.Member)] + Icon = Constants.Icons.Member, + ValueEditorIsReusable = true)] public class MemberPickerPropertyEditor : DataEditor { public MemberPickerPropertyEditor( diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs index 20bc2eb120..79f9c6795b 100644 --- a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs @@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "userpicker", ValueType = ValueTypes.Integer, Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.User)] + Icon = Constants.Icons.User, + ValueEditorIsReusable = true)] public class UserPickerPropertyEditor : DataEditor { public UserPickerPropertyEditor( diff --git a/src/Umbraco.Core/Services/CultureImpactFactory.cs b/src/Umbraco.Core/Services/CultureImpactFactory.cs index c520f95d0e..a05a030d1b 100644 --- a/src/Umbraco.Core/Services/CultureImpactFactory.cs +++ b/src/Umbraco.Core/Services/CultureImpactFactory.cs @@ -1,25 +1,33 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; public class CultureImpactFactory : ICultureImpactFactory { - private SecuritySettings _securitySettings; + private ContentSettings _contentSettings; - public CultureImpactFactory(IOptionsMonitor securitySettings) + public CultureImpactFactory(IOptionsMonitor contentSettings) { - _securitySettings = securitySettings.CurrentValue; + _contentSettings = contentSettings.CurrentValue; - securitySettings.OnChange(x => _securitySettings = x); + contentSettings.OnChange(x => _contentSettings = x); + } + + [Obsolete("Use constructor that takes IOptionsMonitor instead. Scheduled for removal in V12")] + public CultureImpactFactory(IOptionsMonitor securitySettings) + : this(StaticServiceProvider.Instance.GetRequiredService>()) + { } /// public CultureImpact? Create(string? culture, bool isDefault, IContent content) { - TryCreate(culture, isDefault, content.ContentType.Variations, true, _securitySettings.AllowEditInvariantFromNonDefault, out CultureImpact? impact); + TryCreate(culture, isDefault, content.ContentType.Variations, true, _contentSettings.AllowEditInvariantFromNonDefault, out CultureImpact? impact); return impact; } @@ -48,7 +56,7 @@ public class CultureImpactFactory : ICultureImpactFactory throw new ArgumentException("Culture \"*\" is not explicit."); } - return new CultureImpact(culture, isDefault, _securitySettings.AllowEditInvariantFromNonDefault); + return new CultureImpact(culture, isDefault, _contentSettings.AllowEditInvariantFromNonDefault); } /// diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index b7d600ec7c..dd5b77abec 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -57,7 +57,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddTransient(); builder.Services.AddUnique(); builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddSingleton(); return builder; } 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/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 3dc5483266..b4f7b12563 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -2,6 +2,7 @@ using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; @@ -21,7 +22,14 @@ public class AlterTableBuilder : ExpressionBuilderBase Expression.Execute(); + public void Do() + { + if (_context.Database.DatabaseType.IsSqlite()) + { + throw new NotSupportedException($"SQLite does not support ALTER TABLE operations. Instead you will have to:{Environment.NewLine}1. Create a temp table.{Environment.NewLine}2. Copy data from existing table into the temp table.{Environment.NewLine}3. Delete the existing table.{Environment.NewLine}4. Create a new table with the name of the table you're trying to alter, but with a new signature{Environment.NewLine}5. Copy data from the temp table into the new table.{Environment.NewLine}6. Delete the temp table."); + } + Expression.Execute(); + } public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index c49f0d2963..200af7ad70 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -483,8 +483,7 @@ public class MemberRepository : ContentRepositoryBase - "umbracoNode.id = @id"; + => "umbracoNode.id = @id"; // TODO: document/understand that one protected Sql GetNodeIdQueryWithPropertyData() => @@ -519,6 +518,8 @@ public class MemberRepository : ContentRepositoryBase deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity), key = entity.Key }); + } + + entity.DeleteDate = DateTime.Now; + } protected override void PersistNewItem(IUser entity) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index 70a0aa35dc..f36d7b67ff 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "blocklist", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-thumbnail-list")] + Icon = "icon-thumbnail-list", + ValueEditorIsReusable = false)] public class BlockListPropertyEditor : BlockEditorPropertyEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs index 76a7fb5b6d..e64a7fe16c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Checkbox list", "checkboxlist", Icon = "icon-bulleted-list", - Group = Constants.PropertyEditors.Groups.Lists)] + Group = Constants.PropertyEditors.Groups.Lists, + ValueEditorIsReusable = true)] public class CheckBoxListPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs index 1ff39654b1..1ce8ae4930 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Color Picker", "colorpicker", Icon = "icon-colorpicker", - Group = Constants.PropertyEditors.Groups.Pickers)] + Group = Constants.PropertyEditors.Groups.Pickers, + ValueEditorIsReusable = true)] public class ColorPickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs index b6c55ebb6c..e4fedf37ea 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Date/Time", "datepicker", ValueType = ValueTypes.DateTime, - Icon = "icon-time")] + Icon = "icon-time", + ValueEditorIsReusable = true)] public class DateTimePropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs index aca49d2f42..831f858fb8 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Dropdown", "dropdownFlexible", Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-indent")] + Icon = "icon-indent", + ValueEditorIsReusable = true)] public class DropDownFlexiblePropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs index 1561c63e3c..6edcb61f4d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs @@ -12,7 +12,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; EditorType.PropertyValue | EditorType.MacroParameter, "Email address", "email", - Icon = "icon-message")] + Icon = "icon-message", + ValueEditorIsReusable = true)] public class EmailAddressPropertyEditor : DataEditor { private readonly IIOHelper _ioHelper; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs index 1e5972f41f..a2cf5ef6e9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs @@ -21,7 +21,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "File upload", "fileupload", Group = Constants.PropertyEditors.Groups.Media, - Icon = "icon-download-alt")] + Icon = "icon-download-alt", + ValueEditorIsReusable = true)] public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index bd3f5423ee..d2281b0136 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -28,7 +28,8 @@ namespace Umbraco.Cms.Core.PropertyEditors HideLabel = true, ValueType = ValueTypes.Json, Icon = "icon-layout", - Group = Constants.PropertyEditors.Groups.RichContent)] + Group = Constants.PropertyEditors.Groups.RichContent, + ValueEditorIsReusable = false)] public class GridPropertyEditor : DataEditor { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index f7b966e3ad..c3390b3fc5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -28,7 +28,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; ValueType = ValueTypes.Json, HideLabel = false, Group = Constants.PropertyEditors.Groups.Media, - Icon = "icon-crop")] + Icon = "icon-crop", + ValueEditorIsReusable = true)] public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ListViewPropertyEditor.cs index f027b9edd3..b1b7c5c034 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ListViewPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "listview", HideLabel = true, Group = Constants.PropertyEditors.Groups.Lists, - Icon = Constants.Icons.ListView)] + Icon = Constants.Icons.ListView, + ValueEditorIsReusable = true)] public class ListViewPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index 653c88f1c3..ed774f9215 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -24,7 +24,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "mediapicker3", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Media, - Icon = Constants.Icons.MediaImage)] + Icon = Constants.Icons.MediaImage, + ValueEditorIsReusable = true)] public class MediaPicker3PropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs index ccc604ef72..5cbc8e91a0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs @@ -26,7 +26,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Media, Icon = Constants.Icons.MediaImage, - IsDeprecated = false)] + IsDeprecated = false, + ValueEditorIsReusable = true)] public class MediaPickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 924f6b6940..1e20d8cfec 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "contentpicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Pickers, - Icon = "icon-page-add")] + Icon = "icon-page-add", + ValueEditorIsReusable = true)] public class MultiNodeTreePickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs index 4ffed0c1da..7387ab7808 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -16,7 +16,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "multiurlpicker", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, - Icon = "icon-link")] + Icon = "icon-link", + ValueEditorIsReusable = true)] public class MultiUrlPickerPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs index e80da62e9b..4f25a54162 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -25,7 +25,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "multipletextbox", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-ordered-list")] + Icon = "icon-ordered-list", + ValueEditorIsReusable = true)] public class MultipleTextStringPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 880c77134f..230c6e2b59 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -25,7 +25,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "nestedcontent", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-thumbnail-list")] + Icon = "icon-thumbnail-list", + ValueEditorIsReusable = false)] public class NestedContentPropertyEditor : DataEditor { public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs index 4fcfb04126..f121e665fe 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "radiobuttons", ValueType = ValueTypes.String, Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-target")] + Icon = "icon-target", + ValueEditorIsReusable = true)] public class RadioButtonsPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 98f2d028ea..8525de17b6 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -29,7 +29,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; ValueType = ValueTypes.Text, HideLabel = false, Group = Constants.PropertyEditors.Groups.RichContent, - Icon = "icon-browser-window")] + Icon = "icon-browser-window", + ValueEditorIsReusable = true)] public class RichTextPropertyEditor : DataEditor { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs index 48bfb90a39..4ac27824ba 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs @@ -15,7 +15,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; Constants.PropertyEditors.Aliases.Slider, "Slider", "slider", - Icon = "icon-navigation-horizontal")] + Icon = "icon-navigation-horizontal", + ValueEditorIsReusable = true)] public class SliderPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 8357db5b6b..88648c47fd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -23,7 +23,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; Constants.PropertyEditors.Aliases.Tags, "Tags", "tags", - Icon = "icon-tags")] + Icon = "icon-tags", + ValueEditorIsReusable = true)] public class TagsPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs index d72f3cb098..acc33a233b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "Textarea", "textarea", ValueType = ValueTypes.Text, - Icon = "icon-application-window-alt")] + Icon = "icon-application-window-alt", + ValueEditorIsReusable = true)] public class TextAreaPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs index 4f81bf410a..bc340b58ba 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; EditorType.PropertyValue | EditorType.MacroParameter, "Textbox", "textbox", - Group = Constants.PropertyEditors.Groups.Common)] + Group = Constants.PropertyEditors.Groups.Common, + ValueEditorIsReusable = true)] public class TextboxPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs index 70ad112470..0a96a3dcee 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors; "boolean", ValueType = ValueTypes.Integer, Group = Constants.PropertyEditors.Groups.Common, - Icon = "icon-checkbox")] + Icon = "icon-checkbox", + ValueEditorIsReusable = true)] public class TrueFalsePropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index cd684c047a..bd52d43d8b 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -107,7 +107,7 @@ public class MemberUserStore : UmbracoUserStore x.RoleId).ToArray(); + _memberService.ReplaceRoles(new[] { found.Id }, identityUserRoles); + } } if (isLoginsPropertyDirty) @@ -662,9 +668,10 @@ public class MemberUserStore : UmbracoUserStore x.RoleId).ToArray(); - _memberService.ReplaceRoles(new[] { member.Id }, identityUserRoles); + updateRoles = true; } // reset all changes diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 7e038b17cf..4b84af846e 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -16,7 +16,6 @@ TRACE_SCOPES; - @@ -119,6 +118,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..0e0cb355cb --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj @@ -0,0 +1,17 @@ + + + + net7.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..37a91d4599 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net7.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..8ffd5cdca0 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs @@ -0,0 +1,36 @@ +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); + + if (identityUser is not null) + { + 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..d9826126de --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + false + nullable + false + + + + + + + + diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index 7a440ef768..d8a5c0bc04 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -243,7 +243,7 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab IPublishedContent? rootNode = GetByRoute(preview, "/", true); if (rootNode == null) { - throw new Exception("Failed to get node at /."); + throw new Exception("Failed to get node at /. This might be because you're trying to publish a variant, with no domains setup"); } // remove only if we're the default node diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 640a03f447..ae0b9c961f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -24,6 +24,7 @@ using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.BackOffice.Install; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; @@ -211,6 +212,12 @@ public class BackOfficeController : UmbracoController { // force authentication to occur since this is not an authorized endpoint AuthenticateResult result = await this.AuthenticateBackOfficeAsync(); + if (result.Succeeded) + { + // Redirect to installer if we're already authorized + var installerUrl = Url.Action(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Cms.Core.Constants.Web.Mvc.InstallArea }) ?? "/"; + return new LocalRedirectResult(installerUrl); + } var viewPath = Path.Combine(Constants.SystemDirectories.Umbraco, Constants.Web.Mvc.BackOfficeArea, nameof(AuthorizeUpgrade) + ".cshtml"); diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index c24bbc8a20..c8a3c710ec 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -569,7 +569,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers {"minimumPasswordNonAlphaNum", _memberPasswordConfigurationSettings.GetMinNonAlphaNumericChars()}, {"sanitizeTinyMce", _globalSettings.SanitizeTinyMce}, {"dataTypesCanBeChanged", _dataTypesSettings.CanBeChanged.ToString()}, - {"allowEditInvariantFromNonDefault", _securitySettings.AllowEditInvariantFromNonDefault}, + {"allowEditInvariantFromNonDefault", _contentSettings.AllowEditInvariantFromNonDefault}, } }, { diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index ab1b19dde8..52068c6f8d 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] @@ -59,26 +60,27 @@ public class InstallApiController : ControllerBase internal InstallHelper InstallHelper { get; } public bool PostValidateDatabaseConnection(DatabaseModel databaseSettings) - => _databaseBuilder.ConfigureDatabaseConnection(databaseSettings, true); + { + if (_runtime.State.Level != RuntimeLevel.Install) + { + return false; + } + + return _databaseBuilder.ConfigureDatabaseConnection(databaseSettings, true); + } /// - /// Gets the install setup. + /// Gets the install setup. /// public InstallSetup GetSetup() { - var setup = new InstallSetup(); + // Only get the steps that are targeting the current install type + var setup = new InstallSetup + { + Steps = _installSteps.GetStepsForCurrentInstallType().ToList() + }; - // TODO: Check for user/site token - - var steps = new List(); - - InstallSetupStep[] installSteps = _installSteps.GetStepsForCurrentInstallType().ToArray(); - - //only get the steps that are targeting the current install type - steps.AddRange(installSteps); - setup.Steps = steps; - - _installStatusTracker.Initialize(setup.InstallId, installSteps); + _installStatusTracker.Initialize(setup.InstallId, setup.Steps); return setup; } @@ -86,20 +88,23 @@ public class InstallApiController : ControllerBase [HttpPost] public async Task CompleteInstall() { + RuntimeLevel levelBeforeRestart = _runtime.State.Level; + await _runtime.RestartAsync(); - BackOfficeIdentityUser? identityUser = await _backOfficeUserManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); - if (identityUser is not null) + if (levelBeforeRestart == RuntimeLevel.Install) { - _backOfficeSignInManager.SignInAsync(identityUser, false); + BackOfficeIdentityUser? identityUser = + await _backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserIdAsString); + if (identityUser is not null) + { + _backOfficeSignInManager.SignInAsync(identityUser, false); + } } return NoContent(); } - /// - /// Installs. - /// public async Task> PostPerformInstall(InstallInstructions installModel) { if (installModel == null) @@ -107,14 +112,14 @@ public class InstallApiController : ControllerBase throw new ArgumentNullException(nameof(installModel)); } + // There won't be any statuses returned if the app pool has restarted so we need to re-read from file InstallTrackingItem[] status = InstallStatusTracker.GetStatus().ToArray(); - //there won't be any statuses returned if the app pool has restarted so we need to re-read from file. if (status.Any() == false) { status = _installStatusTracker.InitializeFromFile(installModel.InstallId).ToArray(); } - //create a new queue of the non-finished ones + // Create a new queue of the non-finished ones var queue = new Queue(status.Where(x => x.IsComplete == false)); while (queue.Count > 0) { @@ -141,14 +146,15 @@ public class InstallApiController : ControllerBase // determine's the next step in the queue and dequeue's any items that don't need to execute var nextStep = IterateSteps(step, queue, installModel.InstallId, installModel); + bool processComplete = string.IsNullOrEmpty(nextStep) && InstallStatusTracker.GetStatus().All(x => x.IsComplete); // check if there's a custom view to return for this step if (setupData != null && setupData.View.IsNullOrWhiteSpace() == false) { - return new InstallProgressResultModel(false, step.Name, nextStep, setupData.View, setupData.ViewModel); + return new InstallProgressResultModel(processComplete, step.Name, nextStep, setupData.View, setupData.ViewModel); } - return new InstallProgressResultModel(false, step.Name, nextStep); + return new InstallProgressResultModel(processComplete, step.Name, nextStep); } catch (Exception ex) { @@ -249,8 +255,7 @@ public class InstallApiController : ControllerBase Attempt modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { - throw new InvalidCastException( - $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); + throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } var model = modelAttempt.Result; @@ -278,8 +283,7 @@ public class InstallApiController : ControllerBase Attempt modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { - throw new InvalidCastException( - $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); + throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } var model = modelAttempt.Result; diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs index 26eb3e9302..0ea55d861d 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; @@ -28,22 +29,14 @@ public class InstallAreaRoutes : IAreaRoutes switch (_runtime.Level) { case var _ when _runtime.EnableInstaller(): + endpoints.MapUmbracoRoute(installPathSegment, Constants.Web.Mvc.InstallArea, "api", includeControllerNameInRoute: false); + endpoints.MapUmbracoRoute(installPathSegment, Constants.Web.Mvc.InstallArea, string.Empty, includeControllerNameInRoute: false); - endpoints.MapUmbracoRoute(installPathSegment, Constants.Web.Mvc.InstallArea, - "api", includeControllerNameInRoute: false); - endpoints.MapUmbracoRoute(installPathSegment, Constants.Web.Mvc.InstallArea, - string.Empty, includeControllerNameInRoute: false); - - // register catch all because if we are in install/upgrade mode then we'll catch everything and redirect - endpoints.MapFallbackToAreaController( - "Redirect", - ControllerExtensions.GetControllerName(), - Constants.Web.Mvc.InstallArea); - + // register catch all because if we are in install/upgrade mode then we'll catch everything + endpoints.MapFallbackToAreaController(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), Constants.Web.Mvc.InstallArea); break; case RuntimeLevel.Run: - // when we are in run mode redirect to the back office if the installer endpoint is hit endpoints.MapGet($"{installPathSegment}/{{controller?}}/{{action?}}", context => { diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs index 428f21932c..2c6d5102e8 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs @@ -1,55 +1,59 @@ +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; /// -/// Ensures authorization occurs for the installer if it has already completed. -/// If install has not yet occurred then the authorization is successful. +/// Specifies the authorization filter that verifies whether the runtime level is , or and a user is logged in. /// public class InstallAuthorizeAttribute : TypeFilterAttribute { - public InstallAuthorizeAttribute() : base(typeof(InstallAuthorizeFilter)) - { - } + public InstallAuthorizeAttribute() + : base(typeof(InstallAuthorizeFilter)) + { } - private class InstallAuthorizeFilter : IAuthorizationFilter + private class InstallAuthorizeFilter : IAsyncAuthorizationFilter { private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; + private readonly LinkGenerator _linkGenerator; + private readonly IHostingEnvironment _hostingEnvironment; - public InstallAuthorizeFilter( - IRuntimeState runtimeState, - ILogger logger) + public InstallAuthorizeFilter(IRuntimeState runtimeState, ILogger logger, LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) { _runtimeState = runtimeState; _logger = logger; + _linkGenerator = linkGenerator; + _hostingEnvironment = hostingEnvironment; } - public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { - if (!IsAllowed(authorizationFilterContext)) + if (_runtimeState.EnableInstaller() == false) { - authorizationFilterContext.Result = new ForbidResult(); + // Only authorize when the installer is enabled + context.Result = new ForbidResult(new AuthenticationProperties() + { + RedirectUri = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment) + }); } - } - - private bool IsAllowed(AuthorizationFilterContext authorizationFilterContext) - { - try + else if (_runtimeState.Level == RuntimeLevel.Upgrade && (await context.HttpContext.AuthenticateBackOfficeAsync()).Succeeded == false) { - // if not configured (install or upgrade) then we can continue - // otherwise we need to ensure that a user is logged in - return _runtimeState.EnableInstaller() - || (authorizationFilterContext.HttpContext.User?.Identity?.IsAuthenticated ?? false); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred determining authorization"); - return false; + // Redirect to authorize upgrade + var authorizeUpgradePath = _linkGenerator.GetPathByAction(nameof(BackOfficeController.AuthorizeUpgrade), ControllerExtensions.GetControllerName(), new + { + area = Constants.Web.Mvc.BackOfficeArea, + redir = _linkGenerator.GetInstallerUrl() + }); + context.Result = new LocalRedirectResult(authorizeUpgradePath ?? "/"); } } } diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index ab6029cc43..a62a96f909 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -1,6 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; @@ -18,16 +16,14 @@ using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; 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 { - private static bool _reported; - private static RuntimeLevel _reportedLevel; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; @@ -62,31 +58,12 @@ public class InstallController : Controller [HttpGet] [StatusCodeResult(HttpStatusCode.ServiceUnavailable)] - [TypeFilter(typeof(StatusCodeResultAttribute), Arguments = new object[] { HttpStatusCode.ServiceUnavailable })] public async Task Index() { - var umbracoPath = Url.GetBackOfficeUrl(); - - if (_runtime.Level == RuntimeLevel.Run) - { - return Redirect(umbracoPath!); - } - - // TODO: Update for package migrations - if (_runtime.Level == RuntimeLevel.Upgrade) - { - AuthenticateResult authResult = await this.AuthenticateBackOfficeAsync(); - - if (!authResult.Succeeded) - { - return Redirect(_globalSettings.UmbracoPath + "/AuthorizeUpgrade?redir=" + Request.GetEncodedUrl()); - } - } - - // gen the install base URL + // Get the install base URL ViewData.SetInstallApiBaseUrl(_linkGenerator.GetInstallerApiUrl()); - // get the base umbraco folder + // Get the base umbraco folder var baseFolder = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath); ViewData.SetUmbracoBaseFolder(baseFolder); @@ -97,33 +74,7 @@ public class InstallController : Controller return View(Path.Combine(Constants.SystemDirectories.Umbraco.TrimStart("~"), Constants.Web.Mvc.InstallArea, nameof(Index) + ".cshtml")); } - /// - /// Used to perform the redirect to the installer when the runtime level is or - /// - /// - /// [HttpGet] [IgnoreFromNotFoundSelectorPolicy] - public ActionResult Redirect() - { - var uri = HttpContext.Request.GetEncodedUrl(); - - // redirect to install - ReportRuntime(_logger, _runtime.Level, "Umbraco must install or upgrade."); - - var installUrl = $"{_linkGenerator.GetInstallerUrl()}?redir=true&url={uri}"; - return Redirect(installUrl); - } - - private static void ReportRuntime(ILogger logger, RuntimeLevel level, string message) - { - if (_reported && _reportedLevel == level) - { - return; - } - - _reported = true; - _reportedLevel = level; - logger.LogWarning(message); - } + public ActionResult Redirect() => NotFound(); } diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 6d8cc02c1a..3d300d4613 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/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index 123e39f5e2..79c60bc230 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -52,6 +52,7 @@ public static partial class UmbracoBuilderExtensions .AddRoleManager() .AddMemberManager() .AddSignInManager() + .AddClaimsPrincipalFactory() .AddErrorDescriber() .AddUserConfirmation>(); diff --git a/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..2af8274b39 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs @@ -0,0 +1,54 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// A for members +/// +public class MemberClaimsPrincipalFactory : UserClaimsPrincipalFactory +{ + /// + /// Initializes a new instance of the class. + /// + /// The user manager + /// The + public MemberClaimsPrincipalFactory( + UserManager userManager, + IOptions optionsAccessor) + : base(userManager, optionsAccessor) + { + } + + protected virtual string AuthenticationType => IdentityConstants.ApplicationScheme; + + /// + protected override async Task GenerateClaimsAsync(MemberIdentityUser user) + { + // Get the base + ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); + + // now create a new one with the correct authentication type + var memberIdentity = new ClaimsIdentity( + AuthenticationType, + Options.ClaimsIdentity.UserNameClaimType, + Options.ClaimsIdentity.RoleClaimType); + + // and merge all others from the base implementation + memberIdentity.MergeAllClaims(baseIdentity); + + // And merge claims added to the user, for instance in OnExternalLogin, we need to do this explicitly, since the claims are IdentityClaims, so it's not handled by memberIdentity. + foreach (Claim claim in user.Claims + .Where(claim => claim.ClaimType is not null && claim.ClaimValue is not null) + .Where(claim => memberIdentity.HasClaim(claim.ClaimType!, claim.ClaimValue!) is false) + .Select(x => new Claim(x.ClaimType!, x.ClaimValue!))) + { + memberIdentity.AddClaim(claim); + } + + return memberIdentity; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index b91baa16c0..4eefa5176d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -189,10 +189,6 @@ return false; } - if (property.$propertyEditorDisabledCache) { - return property.$propertyEditorDisabledCache; - } - var contentLanguage = $scope.content.language; var otherCreatedVariants = $scope.contentNodeModel.variants.filter(x => x.compositeId !== $scope.content.compositeId && (x.state !== "NotCreated" || x.name !== null)).length === 0; @@ -205,7 +201,7 @@ var canEditSegment = property.segment === $scope.content.segment; - return property.$propertyEditorDisabledCache = !canEditCulture || !canEditSegment; + return !canEditCulture || !canEditSegment; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 25e55455db..702cd5aeda 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -19,6 +19,7 @@ }, bindings: { property: "=", + node: "<", elementKey: "@", // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) propertyAlias: "@", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js index cc9f36852a..2d1ff762d5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js @@ -11,6 +11,7 @@ function umbPropEditor(umbPropEditorHelper, localizationService) { return { scope: { model: "=", + node: "<", isPreValue: "@", preview: "<", allowUnlock: "").html(input).text(); - }; + return function (input) { + return $("
    ").html(input).text(); + }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 24432ca261..08c2f93001 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -13,7 +13,7 @@ (function () { 'use strict'; - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService, $compile) { /** * Simple mapping from property model content entry to editing model, @@ -62,8 +62,8 @@ /** * Map property values from an ElementModel to another ElementModel. * Used to tricker watchers for synchronization. - * @param {Object} fromModel ElementModel to recive property values from. - * @param {Object} toModel ElementModel to recive property values from. + * @param {Object} fromModel ElementModel to receive property values from. + * @param {Object} toModel ElementModel to receive property values from. */ function mapElementValues(fromModel, toModel) { if (!fromModel || !fromModel.variants) { @@ -97,40 +97,6 @@ } } - /** - * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockObject BlockObject to receive data values from. - */ - function getBlockLabel(blockObject) { - if (blockObject.labelInterpolator !== undefined) { - // blockobject.content may be null if the block is no longer allowed, - // so try and fall back to the label in the config, - // if that too is null, there's not much we can do, so just default to empty string. - var contentTypeName; - if(blockObject.content != null){ - contentTypeName = blockObject.content.contentTypeName; - } - else if(blockObject.config != null && blockObject.config.label != null){ - contentTypeName = blockObject.config.label; - } - else { - contentTypeName = ""; - } - - var labelVars = Object.assign({ - "$contentTypeName": contentTypeName, - "$settings": blockObject.settingsData || {}, - "$layout": blockObject.layout || {}, - "$index": (blockObject.index || 0)+1 - }, blockObject.data); - var label = blockObject.labelInterpolator(labelVars); - if (label) { - return label; - } - } - return blockObject.content.contentTypeName; - } - /** * Used to add watchers on all properties in a content or settings model */ @@ -161,10 +127,6 @@ } } } - if (blockObject.__watchers.length === 0) { - // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. - blockObject.updateLabel(); - } } /** @@ -176,8 +138,6 @@ // sync data: prop.value = blockObject.data[prop.alias]; - - blockObject.updateLabel(); } } } @@ -203,8 +163,6 @@ // sync data: blockObject.data[prop.alias] = prop.value; } - - blockObject.updateLabel(); } } @@ -322,11 +280,11 @@ * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. * @param {object} blockConfigurations block configurations. - * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} scopeOfExistence A local angularJS scope that exists as long as the data exists. * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. * @returns {BlockEditorModelObject} A instance of BlockEditorModelObject. */ - function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistence, propertyEditorScope) { if (!propertyModelValue) { throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); @@ -358,8 +316,8 @@ }); this.scaffolds = []; - - this.isolatedScope = scopeOfExistance.$new(true); + this.__scopeOfExistence = scopeOfExistence; + this.isolatedScope = scopeOfExistence.$new(true); this.isolatedScope.blockObjects = {}; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); @@ -397,7 +355,7 @@ * @name getBlockConfiguration * @methodOf umbraco.services.blockEditorModelObject * @description Get block configuration object for a given contentElementTypeKey. - * @param {string} key contentElementTypeKey to recive the configuration model for. + * @param {string} key contentElementTypeKey to receive the configuration model for. * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations. */ getBlockConfiguration: function (key) { @@ -477,7 +435,7 @@ * @ngdoc method * @name getAvailableBlocksForBlockPicker * @methodOf umbraco.services.blockEditorModelObject - * @description Retrieve a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * @description Retrieve a list of available blocks, the list containing object with the configuration model(blockConfigModel) and the element type model(elementTypeModel). * The purpose of this data is to provide it for the Block Picker. * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. */ @@ -503,7 +461,7 @@ * @name getScaffoldFromKey * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeKey. - * @param {string} key contentTypeKey to recive the scaffold model for. + * @param {string} key contentTypeKey to receive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ getScaffoldFromKey: function (contentTypeKey) { @@ -515,7 +473,7 @@ * @name getScaffoldFromAlias * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeAlias, used by clipboardService. - * @param {string} alias contentTypeAlias to recive the scaffold model for. + * @param {string} alias contentTypeAlias to receive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ getScaffoldFromAlias: function (contentTypeAlias) { @@ -535,8 +493,7 @@ * - content {Object}: Content model, the content data in a ElementType model. * - settings {Object}: Settings model, the settings data in a ElementType model. * - config {Object}: A local deep copy of the block configuration model. - * - label {string}: The label for this block. - * - updateLabel {Method}: Method to trigger an update of the label for this block. + * - label {string}: The compiled label for this block. * - data {Object}: A reference to the content data object from your property editor model. * - settingsData {Object}: A reference to the settings data object from your property editor model. * - layout {Object}: A reference to the layout entry from your property editor model. @@ -581,18 +538,12 @@ blockObject.key = String.CreateGuid().replace(/-/g, ""); blockObject.config = Utilities.copy(blockConfiguration); if (blockObject.config.label && blockObject.config.label !== "") { - blockObject.labelInterpolator = $interpolate(blockObject.config.label); + /** + * @deprecated use blockObject.label instead + */ + blockObject.labelInterpolator = $interpolate(blockObject.config.label); } blockObject.__scope = this.isolatedScope; - blockObject.updateLabel = _.debounce( - function () { - // Check wether scope still exists, maybe object was destoyed in these seconds. - if (this.__scope) { - this.label = getBlockLabel(this); - this.__scope.$evalAsync(); - } - }.bind(blockObject) - , 10); // make basics from scaffold if(contentScaffold !== null) {// We might not have contentScaffold @@ -655,6 +606,7 @@ if (this.config.settingsElementTypeKey !== null) { mapElementValues(settings, this.settings); } + }; blockObject.sync = function () { @@ -667,7 +619,61 @@ }; // first time instant update of label. - blockObject.label = getBlockLabel(blockObject); + blockObject.label = blockObject.content.contentTypeName; + blockObject.index = 0; + + if (blockObject.config.label && blockObject.config.label !== "") { + var labelElement = $('
    ', { text: blockObject.config.label}); + + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + blockObject.label = mutation.target.textContent; + blockObject.__scope.$evalAsync(); + }); + }); + + observer.observe(labelElement[0], {characterData: true, subtree:true}); + + blockObject.__watchers.push(() => { + observer.disconnect(); + }) + + blockObject.__labelScope = this.__scopeOfExistence.$new(true); + blockObject.__renderLabel = function() { + + var labelVars = { + $contentTypeName: this.content.contentTypeName, + $settings: this.settingsData || {}, + $layout: this.layout || {}, + $index: this.index + 1, + ... this.data + }; + + this.__labelScope = Object.assign(this.__labelScope, labelVars); + + $compile(labelElement.contents())(this.__labelScope); + }.bind(blockObject) + } else { + blockObject.__renderLabel = function() {}; + } + + blockObject.updateLabel = _.debounce(blockObject.__renderLabel, 10); + + + // label rendering watchers: + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.data; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.settingsData; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.layout; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watch(function () { + return blockObject.index; + }, blockObject.__renderLabel)); + // Add blockObject to our isolated scope to enable watching its values: this.isolatedScope.blockObjects["_" + blockObject.key] = blockObject; @@ -679,9 +685,8 @@ this.__watchers.forEach(w => { w(); }); delete this.__watchers; - // help carbage collector: + // help garbage collector: delete this.config; - delete this.layout; delete this.data; delete this.settingsData; @@ -695,6 +700,11 @@ // destroyed. If we do that here it breaks the scope chain and validation. delete this.__scope; + if(this.__labelScope) { + this.__labelScope.$destroy(); + delete this.__labelScope; + } + // removes this method, making it impossible to destroy again. delete this.destroy; @@ -917,6 +927,7 @@ delete this.scaffolds; this.isolatedScope.$destroy(); delete this.isolatedScope; + delete this.__scopeOfExistence; delete this.destroy; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 00871caab1..ee9aa0864f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -8,6 +8,14 @@ angular.module('umbraco.services') // this is used so that we know when to go and get the user's remaining seconds directly. var lastServerTimeoutSet = null; + eventsService.on("editors.languages.languageSaved", () => { + service.refreshCurrentUser(); + }); + + eventsService.on("editors.userGroups.userGroupSaved", () => { + service.refreshCurrentUser(); + }); + function openLoginDialog(isTimedOut) { //broadcast a global event that the user is no longer logged in const args = { isTimedOut: isTimedOut }; @@ -158,7 +166,7 @@ angular.module('umbraco.services') } }); - return { + const service = { /** Internal method to display the login dialog */ _showLoginDialog: function () { @@ -292,4 +300,6 @@ angular.module('umbraco.services') } }; + return service; + }); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less index 97646e57b9..32be2f2245 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less @@ -17,6 +17,8 @@ .umb-group-panel__header h2 { font-size: @fontSizeMedium; font-weight: bold; + line-height: 1.3; + margin: 0; } .umb-group-panel__content { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less index 408100978e..83f1cd8d36 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less @@ -11,8 +11,8 @@ } h1 { - font-size: 18.75px; - font-weight: 600; + font-size: @baseFontSize; + font-weight: 700; margin: 0; width: 100%; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 350483be97..bac1ebc4f3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -38,6 +38,7 @@ .umb-node-preview__content { flex: 1 1 auto; + margin-right: 25px; overflow: hidden; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less index f35998bbf8..b2a5a055e1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less @@ -46,7 +46,7 @@ transform: translate(-50%, 0); max-width: 800px; min-width: 300px; - padding: 12px 20px; + padding: 9px 15px; background: rgba(242,246,255,.8); border: 1px solid @blueMid; color: @blueMid; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property.less index 0d2bd1118a..34d65f0c5d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property.less @@ -1,18 +1,26 @@ -.umb-property:focus-within .umb-property-culture-label, -.umb-property:hover .umb-property-culture-label { +.umb-property:focus-within .umb-property-variant-label-container, +.umb-property:hover .umb-property-variant-label-container { opacity: 1; } -.umb-property:hover .umb-property:not(:hover) .umb-property-culture-label { +.umb-property:hover .umb-property:not(:hover) .umb-property-variant-label-container { opacity: 0; } -.umb-property-culture-label { +.umb-property-variant-label-container { + float: left; + clear: both; + opacity: 0; +} + +.umb-property-variant-label { font-size: 11px; padding: 0 7px; background: @gray-10; border-radius: 3px; - float: left; - clear: both; - opacity: 0; + display: inline-block; +} + +.umb-property-variant-label + .umb-property-variant-label { + margin-right: 3px; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less index b2eca6613d..0790bdd07a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less @@ -1,4 +1,3 @@ -.umb-readonlyvalue { - position: relative; - .umb-property-editor--limit-width(); +.umb-readonlyvalue { + position:relative; } diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 502af15699..6e127f9b9b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -308,48 +308,3 @@ select { #consentSliderWrapper { margin-bottom: 60px; } - -#consentSlider { - width: 300px; - - .noUi-target { - background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); - box-shadow: none; - border-radius: 20px; - height: 8px; - border: 1px solid @inputBorder; - - &:focus, - &:focus-within { - border-color: @inputBorderFocus; - } - } - - .noUi-handle { - cursor: grab; - border-radius: 100px; - border: none; - box-shadow: none; - width: 20px !important; - height: 20px !important; - right: -10px !important; // half the handle width - top: -1px; - background-color: @blueExtraDark; - } - - .noUi-handle::before { - display: none; - } - - .noUi-handle::after { - display: none; - } - - .noUi-value { - cursor: pointer; - } - - .noUi-pips-horizontal { - height: 40px; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 78ccbe0ace..c0ddcd6cdb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -421,7 +421,6 @@ // Limit width of specific property editors .umb-property-editor--limit-width { max-width: @propertyEditorLimitedWidth; - word-break: break-all; } // Horizontal dividers diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 256d7baf0a..e944bba1b2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -16,6 +16,7 @@ white-space: nowrap } +.umb-modalcolumn-header h1, .umb-modalcolumn-header h2 { margin: 0; white-space: nowrap; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 23dc3d7dd5..f161c76ee0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -11,7 +11,7 @@ data-element="property-{{property.alias}}" ng-repeat="property in tab.properties track by property.alias" property="property" - show-inherit="contentNodeModel.variants.length > 1 && !property.culture" + show-inherit="contentNodeModel.variants.length > 1 && property.variation !== 'CultureAndSegment'" inherits-from="defaultVariant.displayName"> - +
    - + + + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 40666133b2..8d0087b395 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -28,10 +28,31 @@
    -
    - - {{ vm.property.culture }} +
    + + + + + + + + + + + {{ vm.property.culture }} + + + + + + + + {{ vm.property.segment }} + Default + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 65530f0595..335b477928 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -1,8 +1,11 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 35c478b297..c71773a04b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, $timeout, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService, $attrs) { + function BlockListController($scope, $timeout, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService, $attrs) { var unsubscribe = []; var modelObject; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 1027b82e51..0dc74d7edf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -1,85 +1,86 @@ (function () { - "use strict"; + "use strict"; - /** - * @ngdoc directive - * @name umbraco.directives.directive:umbBlockListBlock - * @description - * The component to render the view for a block. - * If a stylesheet is used then this uses a ShadowDom to make a scoped element. - * This way the backoffice styling does not collide with the block style. - */ + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListBlock + * @description + * The component to render the view for a block. + * If a stylesheet is used then this uses a ShadowDom to make a scoped element. + * This way the backoffice styling does not collide with the block style. + */ + + angular + .module("umbraco") + .component("umbBlockListBlock", { + controller: BlockListBlockController, + controllerAs: "model", + bindings: { + stylesheet: "@", + view: "@", + block: "=", + api: "<", + index: "<", + parentForm: "<" + }, + require: { + valFormManager: "^^valFormManager" + } + } + ); - angular - .module("umbraco") - .component("umbBlockListBlock", { - controller: BlockListBlockController, - controllerAs: "model", - bindings: { - stylesheet: "@", - view: "@", - block: "=", - api: "<", - index: "<", - parentForm: "<" - }, - require: { - valFormManager: "^^valFormManager" - } - } - ); + function BlockListBlockController($scope, $compile, $element) { + var model = this; - function BlockListBlockController($scope, $compile, $element) { - var model = this; + model.$onInit = function () { + // This is ugly and is only necessary because we are not using components and instead + // relying on ng-include. It is definitely possible to compile the contents + // of the view into the DOM using $templateCache and $http instead of using + // ng - include which means that the controllerAs flows directly to the view. + // This would mean that any custom components would need to be updated instead of relying on $scope. + // Guess we'll leave it for now but means all things need to be copied to the $scope and then all + // primitives need to be watched. - model.$onInit = function () { - // This is ugly and is only necessary because we are not using components and instead - // relying on ng-include. It is definitely possible to compile the contents - // of the view into the DOM using $templateCache and $http instead of using - // ng - include which means that the controllerAs flows directly to the view. - // This would mean that any custom components would need to be updated instead of relying on $scope. - // Guess we'll leave it for now but means all things need to be copied to the $scope and then all - // primitives need to be watched. + // let the Block know about its form + model.block.setParentForm(model.parentForm); - // let the Block know about its form - model.block.setParentForm(model.parentForm); + // let the Block know about the current index + model.block.index = model.index; - // let the Block know about the current index - model.block.index = model.index; + $scope.block = model.block; + $scope.api = model.api; + $scope.index = model.index; + $scope.parentForm = model.parentForm; + $scope.valFormManager = model.valFormManager; - $scope.block = model.block; - $scope.api = model.api; - $scope.index = model.index; - $scope.parentForm = model.parentForm; - $scope.valFormManager = model.valFormManager; - - if (model.stylesheet) { - var shadowRoot = $element[0].attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = ` + if (model.stylesheet) { + var shadowRoot = $element[0].attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = `
    `; - $compile(shadowRoot)($scope); - } - else { - $element.append($compile('
    ')($scope)); - } - }; + $compile(shadowRoot)($scope); + } + else { + $element.append($compile('
    ')($scope)); + } + }; - // We need to watch for changes on primitive types and upate the $scope values. - model.$onChanges = function (changes) { - if (changes.index) { - var index = changes.index.currentValue; - $scope.index = index; + // We need to watch for changes on primitive types and update the $scope values. + model.$onChanges = function (changes) { + if (changes.index) { + var index = changes.index.currentValue; + $scope.index = index; - // let the Block know about the current index: - model.block.index = index; - model.block.updateLabel(); - } - }; - } + // let the Block know about the current index: + if ($scope.block) { + $scope.block.index = index; + } + } + }; + } })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index c7ea562d08..ae611468b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -304,6 +304,14 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time navigationService.reloadSection(section); } } + }).catch(function(error){ + // if someone attempts to add mix listviews across sections (i.e. use a members list view on content types), + // a not-supported exception will be most likely be thrown, at least for the default list views - lets be + // helpful and show a meaningful error message directly in content/content type UI + if(error.data && error.data.ExceptionType && error.data.ExceptionType.indexOf("System.NotSupportedException") > -1) { + $scope.viewLoadedError = error.errorMsg + ": " + error.data.ExceptionMessage; + } + $scope.viewLoaded = true; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 83ec4e9279..ab9464d6b2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -202,7 +202,7 @@ +
    {{viewLoadedError}}
    +
    + + + diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 6f06115dc4..eef2c96ec9 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -1,7 +1,4 @@ - - - - + net7.0 Template diff --git a/templates/UmbracoProject/appsettings.json b/templates/UmbracoProject/appsettings.json index ca96acec7f..85f6c15dc7 100644 --- a/templates/UmbracoProject/appsettings.json +++ b/templates/UmbracoProject/appsettings.json @@ -29,12 +29,10 @@ "SanitizeTinyMce": true }, "Content": { + "AllowEditInvariantFromNonDefault": true, "ContentVersionCleanupPolicy": { "EnableCleanup": true } - }, - "Security": { - "AllowEditInvariantFromNonDefault": true } } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts new file mode 100644 index 0000000000..fd2f0a0972 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts @@ -0,0 +1,195 @@ +/// + +import {MediaBuilder} from 'umbraco-cypress-testhelpers'; + +context('Media', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + cy.umbracoSection("media"); + }); + + function refreshMediaTree() { + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Media")').should("be.visible").rightclick(); + //Needs to wait or it can give an error + cy.wait(1000); + cy.get(".umb-outline").contains("Reload").click(); + } + + it('Create folder', () => { + const folderName = 'Media Folder'; + //Ensures that there is not already an existing folder with the same name + cy.umbracoEnsureMediaNameNotExists(folderName); + + //Action + //Creates folder + cy.get(".dropdown-toggle").contains("Create").click({force: true}); + cy.get('[role="menuitem"]').contains("Folder").click({force: true}); + cy.get('[data-element="editor-name-field"]').type(folderName); + cy.umbracoButtonByLabelKey("buttons_save").click(); + + //Assert + cy.umbracoSuccessNotification().should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(folderName); + }); + + it('Create folder inside of folder', () => { + const folderName = 'Folder'; + const insideFolderName = 'Folder in folder'; + //Ensures that there is not already existing folders with the same names + cy.umbracoEnsureMediaNameNotExists(folderName); + cy.umbracoEnsureMediaNameNotExists(insideFolderName); + + //Action + //Creates the first folder with an API call + const mediaFolder = new MediaBuilder() + .withName(folderName) + .withContentTypeAlias('Folder') + .build() + cy.saveMedia(mediaFolder, null); + //Creates second folder + refreshMediaTree(); + cy.umbracoTreeItem('media', [folderName]).click(); + cy.get(".dropdown-toggle").contains("Create").click({force: true}); + cy.get('[role="menuitem"]').contains("Folder").click({force: true}); + cy.get('[data-element="editor-name-field"]').type(insideFolderName); + cy.umbracoButtonByLabelKey("buttons_save").click(); + + //Assert + cy.umbracoSuccessNotification().should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(folderName); + cy.umbracoEnsureMediaNameNotExists(insideFolderName); + }); + + it('Create image', () => { + const imageName = 'ImageTest'; + //Ensures that there is not already an existing image with the name + cy.umbracoEnsureMediaNameNotExists(imageName); + const umbracoFileValue = {"src": "Umbraco.png,"} + + //Action + const mediaImage = new MediaBuilder() + .withName(imageName) + .withContentTypeAlias('Image') + .addProperty() + .withAlias("umbracoFile") + .withValue(umbracoFileValue) + .done() + .build() + const blob = Cypress.Blob.base64StringToBlob("iVBORw0KGgoAAAANSUhEUgAAADcAAAAjCAYAAAAwnJLLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGpSURBVFhH7ZRNq0FRFIbPbzD3A/wKSUkZmCgzAyUpkhhRyMT8TIwlEylDI2WgJMyMmJAB+SqS5OvVXjY599ad3eyt/dRpnbXW7rSf1upo+GKUnKwoOVlRcrKi5GRFycmKkpMVJScrSk5WhJDr9/uIRqPYbDa8Aux2O2QyGVitVjidTrTbbd55cLvdUKlUUCgUcDqdeNXIR+XYBev1OtxuNzweD1ar1auu6zrK5TK9j8dj+P1+LJdL6jOazSZisRj2+z2v/OajcuxitVoNk8kEwWDQIMdqh8OBcjbFcDiM0WhE+Xw+RyKRoPgXQqwlk3qX+0m320UymcTxeKQnHo/D4XDA5XIhn89jvV7zk0aEl2MrydbvOaVerwefz4fZbIbr9YpqtYp0Oo3L5UL9d4SWY2KRSITik1arhWKxyDNgOp0ilUq9VvgdYeWYUCgUwnA45JUHg8EA2WwW5/OZ8kajgVwuJ+bk2F/RZrPBbDZTZPl2u4XX64XFYoHJZIKmaRQ7nQ5JlEol2O12Oh8IBLBYLPjXjAgxuf9CycmKkpMVJScrSk5WvlgOuANsVZDROrcwfgAAAABJRU5ErkJggg=="); + const testFile = new File([blob], "test.jpg"); + cy.saveMedia(mediaImage, testFile); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-ImageTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(imageName); + }); + + it('Create SVG', () => { + const svgName = 'svgTest'; + //Ensures that there is not already an existing SVG with the name + cy.umbracoEnsureMediaNameNotExists(svgName); + + //Action + const mediaSVG = new MediaBuilder() + .withName(svgName) + .withContentTypeAlias('umbracoMediaVectorGraphics') + .build() + cy.saveMedia(mediaSVG, null); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-svgTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(svgName); + }); + + it('Create Audio', () => { + const audioName = 'audioTest'; + //Ensures that there is not already an existing audio with the name + cy.umbracoEnsureMediaNameNotExists(audioName); + + //Action + const mediaAudio = new MediaBuilder() + .withName(audioName) + .withContentTypeAlias('umbracoMediaAudio') + .build() + cy.saveMedia(mediaAudio, null); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-audioTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(audioName); + }); + + it('Create File', () => { + const fileName = 'fileTest'; + //Ensures that there is not already an existing file with the name + cy.umbracoEnsureMediaNameNotExists(fileName); + + //Action + const mediaFile = new MediaBuilder() + .withName(fileName) + .withContentTypeAlias('File') + .build() + cy.saveMedia(mediaFile, null); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-fileTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(fileName); + }); + + it('Create Video', () => { + const videoName = 'videoTest'; + //Ensures that there is not already an existing video with the name + cy.umbracoEnsureMediaNameNotExists(videoName); + + //Action + const mediaVideo = new MediaBuilder() + .withName(videoName) + .withContentTypeAlias('umbracoMediaVideo') + .build() + cy.saveMedia(mediaVideo, null); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-videoTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(videoName); + }); + + it('Create Article', () => { + const articleName = 'articleTest'; + //Ensures that there is not already an existing article with the name + cy.umbracoEnsureMediaNameNotExists(articleName); + + //Action + const mediaArticle = new MediaBuilder() + .withName(articleName) + .withContentTypeAlias('umbracoMediaArticle') + .build() + cy.saveMedia(mediaArticle, null); + refreshMediaTree(); + + //Assert + cy.get('[data-element="tree-item-articleTest"]').should("be.visible"); + + //Cleans up + cy.umbracoEnsureMediaNameNotExists(articleName); + }); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts index 1bdd164f8c..8a6235d1e4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts @@ -214,9 +214,9 @@ import { cy.get('.umb-group-builder__tab-sort-order > .umb-property-editor-tiny').first().type('3'); cy.get('[alias="reorder"]').click(); //Assert - cy.get('.umb-group-builder__group-title-input').eq(0).invoke('attr', 'title').should('eq', 'aTab 2') - cy.get('.umb-group-builder__group-title-input').eq(1).invoke('attr', 'title').should('eq', 'aTab 3') - cy.get('.umb-group-builder__group-title-input').eq(2).invoke('attr', 'title').should('eq', 'aTab 1') + cy.get('.umb-group-builder__tab-name').eq(0).invoke('attr', 'title').should('eq', 'aTab 2') + cy.get('.umb-group-builder__tab-name').eq(1).invoke('attr', 'title').should('eq', 'aTab 3') + cy.get('.umb-group-builder__group-title-input').eq(0).invoke('attr', 'title').should('eq', 'aTab 1') }); it('Reorders groups in a tab', () => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 1df679c2a5..e0572c1cb3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -16,7 +16,7 @@ "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-69" + "umbraco-cypress-testhelpers": "^1.0.0-beta-73" } }, "node_modules/@cypress/request": { @@ -2176,9 +2176,9 @@ } }, "node_modules/umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-69", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-69.tgz", - "integrity": "sha512-2IM+C2XtmiA3txyWatZxgKuNxLdcKLGKICPf0ZqYbOrPeSxTiIPAM9tuoh3heDP6/CdtUnvpaiTUl1c8O6A5Fw==", + "version": "1.0.0-beta-73", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-73.tgz", + "integrity": "sha512-VZy7QFjY5o1oTWdpYGb9xrwr4qUw5BcbEwz0GYZexiKCr+Vqq3MllmLMWfkRl4/9O/tbu+ggKx3OZ49GRAGUyg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -3964,9 +3964,9 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-69", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-69.tgz", - "integrity": "sha512-2IM+C2XtmiA3txyWatZxgKuNxLdcKLGKICPf0ZqYbOrPeSxTiIPAM9tuoh3heDP6/CdtUnvpaiTUl1c8O6A5Fw==", + "version": "1.0.0-beta-73", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-73.tgz", + "integrity": "sha512-VZy7QFjY5o1oTWdpYGb9xrwr4qUw5BcbEwz0GYZexiKCr+Vqq3MllmLMWfkRl4/9O/tbu+ggKx3OZ49GRAGUyg==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index e087d3f3a2..45ef9bef67 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -14,7 +14,7 @@ "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-69" + "umbraco-cypress-testhelpers": "^1.0.0-beta-73" }, "dependencies": { "typescript": "^3.9.2" diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 2dc76704c1..5babe9aa0b 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -104,7 +105,7 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); - configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); + SetUpTestConfiguration(configBuilder); Configuration = configBuilder.Build(); }) @@ -193,4 +194,12 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase } protected virtual T GetRequiredService() => Services.GetRequiredService(); + + protected virtual void SetUpTestConfiguration(IConfigurationBuilder configBuilder) + { + if (GlobalSetupTeardown.TestConfiguration is not null) + { + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); + } + } } 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/Models/CultureImpactTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/CultureImpactTests.cs index 2439c71a8a..0ce0f73271 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/CultureImpactTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/CultureImpactTests.cs @@ -13,24 +13,25 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models; [TestFixture] public class CultureImpactTests { - private CultureImpactFactory BasicImpactFactory => createCultureImpactService(); + private CultureImpactFactory BasicImpactFactory => createCultureImpactService(); [Test] public void Get_Culture_For_Invariant_Errors() { - var result = BasicImpactFactory.GetCultureForInvariantErrors( + var result = BasicImpactFactory.GetCultureForInvariantErrors( Mock.Of(x => x.Published == true), new[] { "en-US", "fr-FR" }, "en-US"); Assert.AreEqual("en-US", result); // default culture is being saved so use it - result = BasicImpactFactory.GetCultureForInvariantErrors( + result = BasicImpactFactory.GetCultureForInvariantErrors( Mock.Of(x => x.Published == false), new[] { "fr-FR" }, "en-US"); - Assert.AreEqual("fr-FR", result); // default culture not being saved with not published version, use the first culture being saved + Assert.AreEqual("fr-FR", + result); // default culture not being saved with not published version, use the first culture being saved - result = BasicImpactFactory.GetCultureForInvariantErrors( + result = BasicImpactFactory.GetCultureForInvariantErrors( Mock.Of(x => x.Published == true), new[] { "fr-FR" }, "en-US"); @@ -70,7 +71,7 @@ public class CultureImpactTests [Test] public void Explicit_Default_Culture() { - var impact = BasicImpactFactory.ImpactExplicit("en-US", true); + var impact = BasicImpactFactory.ImpactExplicit("en-US", true); Assert.AreEqual(impact.Culture, "en-US"); @@ -85,7 +86,7 @@ public class CultureImpactTests [Test] public void Explicit_NonDefault_Culture() { - var impact = BasicImpactFactory.ImpactExplicit("en-US", false); + var impact = BasicImpactFactory.ImpactExplicit("en-US", false); Assert.AreEqual(impact.Culture, "en-US"); @@ -100,10 +101,11 @@ public class CultureImpactTests [Test] public void TryCreate_Explicit_Default_Culture() { - var success = BasicImpactFactory.TryCreate("en-US", true, ContentVariation.Culture, false, false, out var impact); + var success = + BasicImpactFactory.TryCreate("en-US", true, ContentVariation.Culture, false, false, out var impact); Assert.IsTrue(success); - Assert.IsNotNull(impact); + Assert.IsNotNull(impact); Assert.AreEqual(impact.Culture, "en-US"); Assert.IsTrue(impact.ImpactsInvariantProperties); @@ -117,10 +119,11 @@ public class CultureImpactTests [Test] public void TryCreate_Explicit_NonDefault_Culture() { - var success = BasicImpactFactory.TryCreate("en-US", false, ContentVariation.Culture, false, false, out var impact); + var success = + BasicImpactFactory.TryCreate("en-US", false, ContentVariation.Culture, false, false, out var impact); Assert.IsTrue(success); - Assert.IsNotNull(impact); + Assert.IsNotNull(impact); Assert.AreEqual(impact.Culture, "en-US"); Assert.IsFalse(impact.ImpactsInvariantProperties); @@ -137,10 +140,10 @@ public class CultureImpactTests var success = BasicImpactFactory.TryCreate("*", false, ContentVariation.Nothing, false, false, out var impact); Assert.IsTrue(success); - Assert.IsNotNull(impact); + Assert.IsNotNull(impact); Assert.AreEqual(impact.Culture, null); - Assert.AreSame(BasicImpactFactory.ImpactInvariant(), impact); + Assert.AreSame(BasicImpactFactory.ImpactInvariant(), impact); } [Test] @@ -149,10 +152,10 @@ public class CultureImpactTests var success = BasicImpactFactory.TryCreate("*", false, ContentVariation.Culture, false, false, out var impact); Assert.IsTrue(success); - Assert.IsNotNull(impact); + Assert.IsNotNull(impact); Assert.AreEqual(impact.Culture, "*"); - Assert.AreSame(BasicImpactFactory.ImpactAll(), impact); + Assert.AreSame(BasicImpactFactory.ImpactAll(), impact); } [Test] @@ -168,28 +171,27 @@ public class CultureImpactTests var success = BasicImpactFactory.TryCreate(null, false, ContentVariation.Nothing, false, false, out var impact); Assert.IsTrue(success); - Assert.AreSame(BasicImpactFactory.ImpactInvariant(), impact); - } - - [Test] - [TestCase(true)] - [TestCase(false)] - public void Edit_Invariant_From_Non_Default_Impacts_Invariant_Properties(bool allowEditInvariantFromNonDefault) - { - var sut = createCultureImpactService(new SecuritySettings { AllowEditInvariantFromNonDefault = allowEditInvariantFromNonDefault }); - var impact = sut.ImpactExplicit("da", false); - - Assert.AreEqual(allowEditInvariantFromNonDefault, impact.ImpactsAlsoInvariantProperties); + Assert.AreSame(BasicImpactFactory.ImpactInvariant(), impact); } - private CultureImpactFactory createCultureImpactService(SecuritySettings securitySettings = null) + [Test] + [TestCase(true)] + [TestCase(false)] + public void Edit_Invariant_From_Non_Default_Impacts_Invariant_Properties(bool allowEditInvariantFromNonDefault) + { + var sut = createCultureImpactService(new ContentSettings { - securitySettings ??= new SecuritySettings - { - AllowEditInvariantFromNonDefault = false, - }; + AllowEditInvariantFromNonDefault = allowEditInvariantFromNonDefault + }); + var impact = sut.ImpactExplicit("da", false); - return new CultureImpactFactory(new TestOptionsMonitor(securitySettings)); - } + Assert.AreEqual(allowEditInvariantFromNonDefault, impact.ImpactsAlsoInvariantProperties); + } + private CultureImpactFactory createCultureImpactService(ContentSettings contentSettings = null) + { + contentSettings ??= new ContentSettings { AllowEditInvariantFromNonDefault = false, }; + + return new CultureImpactFactory(new TestOptionsMonitor(contentSettings)); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs new file mode 100644 index 0000000000..46f79ebebc --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -0,0 +1,132 @@ +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class DataValueEditorReuseTests +{ + private Mock _dataValueEditorFactoryMock; + private PropertyEditorCollection _propertyEditorCollection; + + [SetUp] + public void SetUp() + { + _dataValueEditorFactoryMock = new Mock(); + + _dataValueEditorFactoryMock + .Setup(m => m.Create(It.IsAny())) + .Returns(() => new TextOnlyValueEditor( + new DataEditorAttribute("a", "b", "c"), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of())); + + _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); + + _dataValueEditorFactoryMock + .Setup(m => + m.Create(It.IsAny())) + .Returns(() => new BlockEditorPropertyEditor.BlockEditorPropertyValueEditor( + new DataEditorAttribute("a", "b", "c"), + _propertyEditorCollection, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of>(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of())); + } + + [Test] + public void GetValueEditor_Reusable_Value_Editor_Is_Reused_When_Created_Without_Configuration() + { + var textboxPropertyEditor = new TextboxPropertyEditor( + _dataValueEditorFactoryMock.Object, + Mock.Of(), + Mock.Of()); + + // textbox is set to reuse its data value editor when created *without* configuration + var dataValueEditor1 = textboxPropertyEditor.GetValueEditor(); + Assert.NotNull(dataValueEditor1); + var dataValueEditor2 = textboxPropertyEditor.GetValueEditor(); + Assert.NotNull(dataValueEditor2); + Assert.AreSame(dataValueEditor1, dataValueEditor2); + _dataValueEditorFactoryMock.Verify( + m => m.Create(It.IsAny()), + Times.Once); + } + + [Test] + public void GetValueEditor_Reusable_Value_Editor_Is_Not_Reused_When_Created_With_Configuration() + { + var textboxPropertyEditor = new TextboxPropertyEditor( + _dataValueEditorFactoryMock.Object, + Mock.Of(), + Mock.Of()); + + // no matter what, a property editor should never reuse its data value editor when created *with* configuration + var dataValueEditor1 = textboxPropertyEditor.GetValueEditor("config"); + Assert.NotNull(dataValueEditor1); + Assert.AreEqual("config", ((DataValueEditor)dataValueEditor1).Configuration); + var dataValueEditor2 = textboxPropertyEditor.GetValueEditor("config"); + Assert.NotNull(dataValueEditor2); + Assert.AreEqual("config", ((DataValueEditor)dataValueEditor2).Configuration); + Assert.AreNotSame(dataValueEditor1, dataValueEditor2); + _dataValueEditorFactoryMock.Verify( + m => m.Create(It.IsAny()), + Times.Exactly(2)); + } + + [Test] + public void GetValueEditor_Not_Reusable_Value_Editor_Is_Not_Reused_When_Created_Without_Configuration() + { + var blockListPropertyEditor = new BlockListPropertyEditor( + _dataValueEditorFactoryMock.Object, + new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + Mock.Of(), + Mock.Of()); + + // block list is *not* set to reuse its data value editor + var dataValueEditor1 = blockListPropertyEditor.GetValueEditor(); + Assert.NotNull(dataValueEditor1); + var dataValueEditor2 = blockListPropertyEditor.GetValueEditor(); + Assert.NotNull(dataValueEditor2); + Assert.AreNotSame(dataValueEditor1, dataValueEditor2); + _dataValueEditorFactoryMock.Verify( + m => m.Create(It.IsAny()), + Times.Exactly(2)); + } + + [Test] + public void GetValueEditor_Not_Reusable_Value_Editor_Is_Not_Reused_When_Created_With_Configuration() + { + var blockListPropertyEditor = new BlockListPropertyEditor( + _dataValueEditorFactoryMock.Object, + new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + Mock.Of(), + Mock.Of()); + + // no matter what, a property editor should never reuse its data value editor when created *with* configuration + var dataValueEditor1 = blockListPropertyEditor.GetValueEditor("config"); + Assert.NotNull(dataValueEditor1); + Assert.AreEqual("config", ((DataValueEditor)dataValueEditor1).Configuration); + var dataValueEditor2 = blockListPropertyEditor.GetValueEditor("config"); + Assert.NotNull(dataValueEditor2); + Assert.AreEqual("config", ((DataValueEditor)dataValueEditor2).Configuration); + Assert.AreNotSame(dataValueEditor1, dataValueEditor2); + _dataValueEditorFactoryMock.Verify( + m => m.Create(It.IsAny()), + Times.Exactly(2)); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs index d525ae0f9a..9d7d95554a 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 fdc40c3e2b..e72ea9c9c6 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}