From 65c828390c533aad9dd6ad0fa63611edbccac9c3 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Aug 2022 11:10:45 +0200 Subject: [PATCH 01/49] CodeQL should not scan compiled assets (#12861) * add the Umbraco.Cms.StaticAssets folder to ignored paths for the codeql scanner * exclude only the 'wwwroot' folder since other assets could be valid to scan --- .github/config/codeql-config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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' From c4c27ec72b7c9b792490d5c56d2a7ea53056eb54 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Aug 2022 14:35:21 +0200 Subject: [PATCH 02/49] add GITHUB_TOKEN restrictive permissions --- .github/workflows/codeql-analysis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c686f373e1..777972d101 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 From c7813aa3549ac6ead0a3f6017304db98bda3b031 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Aug 2022 14:35:43 +0200 Subject: [PATCH 03/49] upgrade codeql tasks to v2 --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 777972d101..e5cd790234 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,7 +24,7 @@ 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 @@ -37,4 +37,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 From 173c2318592474787064b01625c6d6b4c7f3377b Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Aug 2022 14:36:19 +0200 Subject: [PATCH 04/49] upgrade setup-dotnet to v2 --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e5cd790234..74e488206e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: config-file: ./.github/config/codeql-config.yml - name: Setup dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' From 72d3e6f17ec43513f6ef86eb9c9d36482ed93a8e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Aug 2022 08:14:11 +0200 Subject: [PATCH 05/49] Fix version of nightly builds --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 637ae8dd6a..0f9ad08c15 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.2.0-alpha.1", + "version": "10.2.0", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 264a1565cadd3f61a7568f29e5a6011f142d4f48 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Aug 2022 09:29:24 +0200 Subject: [PATCH 06/49] Make the error message friendlier when attempting to use unsupported sorting in listviews (#12846) * Make the error message friendlier when attempting to use unsupported sorting in listviews * Revert automatic code formatting * Review changes --- .../views/propertyeditors/listview/listview.controller.js | 8 ++++++++ .../src/views/propertyeditors/listview/listview.html | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) 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}}
+
Date: Mon, 22 Aug 2022 09:45:55 +0200 Subject: [PATCH 07/49] Fix up assertions --- .../cypress/integration/Tabs/tabs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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', () => { From f8e20a9db9901c009315fabdfa6f6dbd5b3fb407 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Aug 2022 15:18:12 +0200 Subject: [PATCH 08/49] V10: Fix foreign key constraints when using external login provider for members (#12872) * Move role saving out of UpdateMemberProperties * Add missing delete clauses * Fix token for users too * Fix deleting user --- .../Repositories/Implement/MemberRepository.cs | 5 +++-- .../Repositories/Implement/UserRepository.cs | 13 ++++++++++++- .../Security/MemberUserStore.cs | 18 +++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index bd08addddb..02d1383bc3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -490,8 +490,7 @@ public class MemberRepository : ContentRepositoryBase - "umbracoNode.id = @id"; + => "umbracoNode.id = @id"; // TODO: document/understand that one protected Sql GetNodeIdQueryWithPropertyData() => @@ -526,6 +525,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/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 40bfc73a3d..3229264d39 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) @@ -673,9 +679,10 @@ public class MemberUserStore : UmbracoUserStore x.RoleId).ToArray(); - _memberService.ReplaceRoles(new[] { member.Id }, identityUserRoles); + updateRoles = true; } // reset all changes From d96c9a77cfe7dd8bdfe87220a0a606f70dcb40de Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Aug 2022 15:48:17 +0200 Subject: [PATCH 09/49] Better exception message when using ALTER TABLE and SQLite (#12848) * Added NotSupportedException when using Alter Table and SQLite. This is considered better than the underlying sql error. * use Environment.NewLine --- .../Expressions/Alter/Table/AlterTableBuilder.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 3dc5483266..fd9dee6745 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 do 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 temp table.{Environment.NewLine}3. Delete existing table.{Environment.NewLine}4. Create new table with existing name but new signature{Environment.NewLine}5. Copy data from temp table into the new table.{Environment.NewLine}6. Delete temp table."); + } + Expression.Execute(); + } public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) { From ea2ecab238919b2fc60e0a12b818a20ed6a8b9e9 Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Tue, 23 Aug 2022 09:52:42 +0200 Subject: [PATCH 10/49] Add RC to version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 0f9ad08c15..9988b6b4bd 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.2.0", + "version": "10.2.0-rc", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 7e7658314c16b96c5dbe7e089ef57acad9718260 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:51:45 +0200 Subject: [PATCH 11/49] Content modal heading fix (#12797) (#12880) * Fixes to modal and group headings * updated modal headings for h1 and h2 * Updated line-height Changed line-height: 0 to line-height: 1.3 and added margin: 0 Co-authored-by: Tiffany Prosser --- .../src/less/components/html/umb-group-panel.less | 2 ++ src/Umbraco.Web.UI.Client/src/less/modals.less | 1 + 2 files changed, 3 insertions(+) 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/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; From b6e0e2df565062033fb8cf541df9de1dd7278810 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:51:45 +0200 Subject: [PATCH 12/49] Content modal heading fix (#12797) (#12880) * Fixes to modal and group headings * updated modal headings for h1 and h2 * Updated line-height Changed line-height: 0 to line-height: 1.3 and added margin: 0 Co-authored-by: Tiffany Prosser --- .../src/less/components/html/umb-group-panel.less | 2 ++ src/Umbraco.Web.UI.Client/src/less/modals.less | 1 + 2 files changed, 3 insertions(+) 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/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; From f3a1c890665b95af84c9659219bea1fc3b228b79 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Aug 2022 14:12:05 +0200 Subject: [PATCH 13/49] Add ClaimsPrincipalFactory and ensure that claims are flowed from the MemberIdentityUser (#12877) --- .../UmbracoBuilder.MembersIdentity.cs | 1 + .../Security/MemberClaimsPrincipalFactory.cs | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs 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..dfc860e467 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs @@ -0,0 +1,53 @@ +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 => memberIdentity.HasClaim(claim.ClaimType, claim.ClaimValue) is false) + .Select(x => new Claim(x.ClaimType, x.ClaimValue))) + { + memberIdentity.AddClaim(claim); + } + + return memberIdentity; + } +} From e7d6b9ef7a9be7940d43011ba5088acd82f2dcb5 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Aug 2022 14:12:05 +0200 Subject: [PATCH 14/49] Add ClaimsPrincipalFactory and ensure that claims are flowed from the MemberIdentityUser (#12877) --- .../UmbracoBuilder.MembersIdentity.cs | 1 + .../Security/MemberClaimsPrincipalFactory.cs | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs 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..dfc860e467 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs @@ -0,0 +1,53 @@ +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 => memberIdentity.HasClaim(claim.ClaimType, claim.ClaimValue) is false) + .Select(x => new Claim(x.ClaimType, x.ClaimValue))) + { + memberIdentity.AddClaim(claim); + } + + return memberIdentity; + } +} From bd40575c32f7ac5e6cddb8fa94651162a186913a Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 24 Aug 2022 12:29:23 +0200 Subject: [PATCH 15/49] Added new media tests --- .../cypress/integration/Media/media.ts | 195 ++++++++++++++++++ .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts 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..d145b6c20b --- /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/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index e087d3f3a2..8efecf5523 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-70" }, "dependencies": { "typescript": "^3.9.2" From 128dd42b4711fad2cc1b0a5324934daa0af7a560 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 Aug 2022 13:52:39 +0200 Subject: [PATCH 16/49] Bugfix: Variant permission languages needs a clear cache to work fully for current user (#12875) * emit event when user group is saved * clear current user cache when languages and user groups are saved --- .../src/common/services/user.service.js | 12 +++++++++++- .../src/views/users/group.controller.js | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 6e8238b431..712aa6fef3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService, overlayService) { + function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService, overlayService, eventsService) { var infiniteMode = $scope.model && $scope.model.infiniteMode; var id = infiniteMode ? $scope.model.id : $routeParams.id; @@ -107,6 +107,11 @@ setSectionIcon(vm.userGroup.sections); makeBreadcrumbs(); vm.page.saveButtonState = "success"; + + eventsService.emit("editors.userGroups.userGroupSaved", { + userGroup: vm.userGroup, + isNew: create + }); } }, function (err) { vm.page.saveButtonState = "error"; From b4f2de79b80bacb90705f1b21a64c02b23a1d6bb Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 Aug 2022 13:52:39 +0200 Subject: [PATCH 17/49] Bugfix: Variant permission languages needs a clear cache to work fully for current user (#12875) * emit event when user group is saved * clear current user cache when languages and user groups are saved (cherry picked from commit 128dd42b4711fad2cc1b0a5324934daa0af7a560) --- .../src/common/services/user.service.js | 12 +++++++++++- .../src/views/users/group.controller.js | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 6e8238b431..712aa6fef3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService, overlayService) { + function UserGroupEditController($scope, $location, $routeParams, userGroupsResource, localizationService, contentEditingHelper, editorService, overlayService, eventsService) { var infiniteMode = $scope.model && $scope.model.infiniteMode; var id = infiniteMode ? $scope.model.id : $routeParams.id; @@ -107,6 +107,11 @@ setSectionIcon(vm.userGroup.sections); makeBreadcrumbs(); vm.page.saveButtonState = "success"; + + eventsService.emit("editors.userGroups.userGroupSaved", { + userGroup: vm.userGroup, + isNew: create + }); } }, function (err) { vm.page.saveButtonState = "error"; From f8e3a7723735487bff7bb50f191e5e4e0605afc4 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Wed, 24 Aug 2022 14:04:34 +0200 Subject: [PATCH 18/49] Update to version 71 --- .../cypress/integration/Media/media.ts | 2 +- .../Umbraco.Tests.AcceptanceTest/package-lock.json | 14 +++++++------- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts index d145b6c20b..fd2f0a0972 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Media/media.ts @@ -1,6 +1,6 @@ /// -import {MediaBuilder} from 'umbraco-cypress-testhelpers"; +import {MediaBuilder} from 'umbraco-cypress-testhelpers'; context('Media', () => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 1df679c2a5..9a895fb28d 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-71" } }, "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-71", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-71.tgz", + "integrity": "sha512-VhEgfboCbiCyk4SfJcgCVYN3dxfsR/5oIbNMkvvW4XQabeY3xBKbJkTT0dNTsoi6e2KBckfKNMviwcy6x0fqaA==", "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-71", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-71.tgz", + "integrity": "sha512-VhEgfboCbiCyk4SfJcgCVYN3dxfsR/5oIbNMkvvW4XQabeY3xBKbJkTT0dNTsoi6e2KBckfKNMviwcy6x0fqaA==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 8efecf5523..2acf69d0aa 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-70" + "umbraco-cypress-testhelpers": "^1.0.0-beta-71" }, "dependencies": { "typescript": "^3.9.2" From 439878883a6555e5dce45ddc8cd5e78e263f4653 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 Aug 2022 16:17:56 +0200 Subject: [PATCH 19/49] V10/bugfix/variant permissions segments (#12890) * Remove null check from MapperContext.SetCulture and .SetSegment We need to be able to set these to null, since null = invariant / default segment * show segment label on property * Add ContentVariation to ContentPropertyDisplay * Add ContentVariation to DocumentTypeDisplay * Change variations to be on ContentTypeBasic.cs * don't cache value * show correct label and unlock text for culture and segment variations * make lock overlay take up less space Co-authored-by: nikolajlauridsen Co-authored-by: Zeegaan --- .../EmbeddedResources/Lang/en_us.xml | 6 ++++- .../ContentEditing/ContentPropertyDisplay.cs | 3 +++ .../Models/ContentEditing/ContentTypeBasic.cs | 3 +++ .../ContentEditing/DocumentTypeDisplay.cs | 3 +++ .../Mapping/ContentPropertyDisplayMapper.cs | 3 +++ .../Mapping/ContentTypeMapDefinition.cs | 4 +++ .../Models/Mapping/MapperContextExtensions.cs | 16 ++--------- .../content/umbtabbedcontent.directive.js | 6 +---- .../property/umbproperty.directive.js | 1 + .../property/umbpropertyeditor.directive.js | 11 ++++++++ .../less/components/umb-property-editor.less | 2 +- .../src/less/components/umb-property.less | 22 ++++++++++----- .../content/umb-tabbed-content.html | 6 +++-- .../property/umb-property-editor.html | 11 ++++++-- .../components/property/umb-property.html | 27 ++++++++++++++++--- 15 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 70aa1c2d5c..9a44528b53 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -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 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/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 @@ -794,6 +797,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/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.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: " 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 + +
+
From bfbc2abca88d3f05f25afe0fc9df2318fdd6210c Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 Aug 2022 16:17:56 +0200 Subject: [PATCH 20/49] V10/bugfix/variant permissions segments (#12890) * Remove null check from MapperContext.SetCulture and .SetSegment We need to be able to set these to null, since null = invariant / default segment * show segment label on property * Add ContentVariation to ContentPropertyDisplay * Add ContentVariation to DocumentTypeDisplay * Change variations to be on ContentTypeBasic.cs * don't cache value * show correct label and unlock text for culture and segment variations * make lock overlay take up less space Co-authored-by: nikolajlauridsen Co-authored-by: Zeegaan --- .../EmbeddedResources/Lang/en_us.xml | 6 ++++- .../ContentEditing/ContentPropertyDisplay.cs | 3 +++ .../Models/ContentEditing/ContentTypeBasic.cs | 3 +++ .../ContentEditing/DocumentTypeDisplay.cs | 3 +++ .../Mapping/ContentPropertyDisplayMapper.cs | 3 +++ .../Mapping/ContentTypeMapDefinition.cs | 4 +++ .../Models/Mapping/MapperContextExtensions.cs | 16 ++--------- .../content/umbtabbedcontent.directive.js | 6 +---- .../property/umbproperty.directive.js | 1 + .../property/umbpropertyeditor.directive.js | 11 ++++++++ .../less/components/umb-property-editor.less | 2 +- .../src/less/components/umb-property.less | 22 ++++++++++----- .../content/umb-tabbed-content.html | 6 +++-- .../property/umb-property-editor.html | 11 ++++++-- .../components/property/umb-property.html | 27 ++++++++++++++++--- 15 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 70aa1c2d5c..9a44528b53 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -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 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/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 @@ -794,6 +797,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/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.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: " 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 + +
+
From a5237def31951f020757d275823f5201c09dd807 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 25 Aug 2022 08:19:36 +0200 Subject: [PATCH 21/49] Update umbraco-cypress-testhelpers version --- .../Umbraco.Tests.AcceptanceTest/package-lock.json | 14 +++++++------- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 9a895fb28d..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-71" + "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-71", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-71.tgz", - "integrity": "sha512-VhEgfboCbiCyk4SfJcgCVYN3dxfsR/5oIbNMkvvW4XQabeY3xBKbJkTT0dNTsoi6e2KBckfKNMviwcy6x0fqaA==", + "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-71", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-71.tgz", - "integrity": "sha512-VhEgfboCbiCyk4SfJcgCVYN3dxfsR/5oIbNMkvvW4XQabeY3xBKbJkTT0dNTsoi6e2KBckfKNMviwcy6x0fqaA==", + "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 2acf69d0aa..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-71" + "umbraco-cypress-testhelpers": "^1.0.0-beta-73" }, "dependencies": { "typescript": "^3.9.2" From 8f6e28e0ad7b845dfc1182c1beef522c27334ab7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 25 Aug 2022 12:31:10 +0200 Subject: [PATCH 22/49] Fixed issue with saving member groups, that was not persisted, if only the member groups was changed. (#12905) --- src/Umbraco.Infrastructure/Security/MemberUserStore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 3229264d39..70efeeb739 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -794,6 +794,7 @@ public class MemberUserStore : UmbracoUserStore Date: Thu, 25 Aug 2022 15:30:56 +0200 Subject: [PATCH 23/49] Fix memory leaks (#12900) * Fix leak for PublicAccessEntry * Fix memory leak for PropertyTypeCollection * Don't clone the lazy property group ID when caching property types, it is explicitly assigned at runtime --- .../Collections/DeepCloneableList.cs | 24 ++----------------- .../EventClearingObservableCollection.cs | 11 ++++++++- src/Umbraco.Core/Models/ContentBase.cs | 8 ------- src/Umbraco.Core/Models/DeepCloneHelper.cs | 12 ++++++++++ .../Models/Entities/BeingDirtyBase.cs | 2 ++ src/Umbraco.Core/Models/PropertyType.cs | 7 +----- src/Umbraco.Core/Models/PublicAccessEntry.cs | 16 +++++++------ 7 files changed, 36 insertions(+), 44 deletions(-) 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/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/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/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; } } From 785c4440f3e3fc2ba684d6dd8d6eb1e714247161 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Aug 2022 15:30:56 +0200 Subject: [PATCH 24/49] Fix memory leaks (#12900) * Fix leak for PublicAccessEntry * Fix memory leak for PropertyTypeCollection * Don't clone the lazy property group ID when caching property types, it is explicitly assigned at runtime (cherry picked from commit 10b8f63052c3c69c24c1d81d8e166003b3fc2db8) --- .../Collections/DeepCloneableList.cs | 24 ++----------------- .../EventClearingObservableCollection.cs | 11 ++++++++- src/Umbraco.Core/Models/ContentBase.cs | 8 ------- src/Umbraco.Core/Models/DeepCloneHelper.cs | 12 ++++++++++ .../Models/Entities/BeingDirtyBase.cs | 2 ++ src/Umbraco.Core/Models/PropertyType.cs | 7 +----- src/Umbraco.Core/Models/PublicAccessEntry.cs | 16 +++++++------ 7 files changed, 36 insertions(+), 44 deletions(-) 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/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/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/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; } } From 6dab24265a839d1052514f2e6736de4a375c1227 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 25 Aug 2022 14:45:11 +0200 Subject: [PATCH 25/49] Fix duplicate Directory.Build.props import --- templates/Directory.Build.props | 4 ++++ templates/Umbraco.Templates.csproj | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 templates/Directory.Build.props diff --git a/templates/Directory.Build.props b/templates/Directory.Build.props new file mode 100644 index 0000000000..c4e40e3e06 --- /dev/null +++ b/templates/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 47b2246835..d01e811cde 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -1,7 +1,4 @@ - - - - + net6.0 Template From a105b3b770ecedc7e72ed829fe0994aa2fc9f23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 26 Aug 2022 15:09:34 +0200 Subject: [PATCH 26/49] v10: make block editors supports stateful label expressions (#12909) * mark ncNodeName as stateful allowing it to update the node name asynchronously and implement several checks for caching and fallthroughs * ensure that the blocklist block component watches and updates stuff on the blockObject * add $interpolate to the blockList Property Editor to interpolate the label with the saved state * replace static label with the blockHtmlCompile directive to ensure labels are updated dynamically * add failsafe in case block is not instantiated * replace manual udi separation with the parse function from the udiParser service * simplify watching, to avoid overwritting data object. * virtual block label rendering * destroy label scope * add extra information for label doc * revert previously used functions and add deprecation notices to them * remove getBlockLabel, as it's not being used or publicly available. Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../common/filters/nestedcontent.filter.js | 107 +++++++------ .../blockeditormodelobject.service.js | 147 ++++++++++-------- .../labelblock/labelblock.editor.html | 17 +- .../umbBlockListPropertyEditor.component.js | 2 +- .../blocklist/umbblocklistblock.component.js | 135 ++++++++-------- 5 files changed, 217 insertions(+), 191 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js index 8c23094bbf..b0ea8be9a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js @@ -3,67 +3,78 @@ // Cache for node names so we don't make a ton of requests var ncNodeNameCache = { - id: "", - keys: {} + id: "", + keys: {} }; -angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource) { +angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource, udiParser) { - function formatLabel(firstNodeName, totalNodes) { - return totalNodes <= 1 - ? firstNodeName - // If there is more than one item selected, append the additional number of items selected to hint that - : firstNodeName + " (+" + (totalNodes - 1) + ")"; + function formatLabel(firstNodeName, totalNodes) { + return totalNodes <= 1 + ? firstNodeName + // If there is more than one item selected, append the additional number of items selected to hint that + : firstNodeName + " (+" + (totalNodes - 1) + ")"; + } + + nodeNameFilter.$stateful = true; + function nodeNameFilter(input) { + + // Check we have a value at all + if (typeof input === 'undefined' || input === "" || input.toString() === "0" || input === null) { + return ""; } - return function (input) { + var currentNode = editorState.getCurrent(); - // Check we have a value at all - if (input === "" || input.toString() === "0") { - return ""; - } + // Ensure a unique cache per editor instance + var key = "ncNodeName_" + currentNode.key; + if (ncNodeNameCache.id !== key) { + ncNodeNameCache.id = key; + ncNodeNameCache.keys = {}; + } - var currentNode = editorState.getCurrent(); + // MNTP values are comma separated IDs. We'll only fetch the first one for the NC header. + var ids = input.split(','); + var lookupId = ids[0]; + var serviceInvoked = false; - // Ensure a unique cache per editor instance - var key = "ncNodeName_" + currentNode.key; - if (ncNodeNameCache.id !== key) { - ncNodeNameCache.id = key; - ncNodeNameCache.keys = {}; - } + // See if there is a value in the cache and use that + if (ncNodeNameCache.keys[lookupId]) { + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } - // MNTP values are comma separated IDs. We'll only fetch the first one for the NC header. - var ids = input.split(','); - var lookupId = ids[0]; + // No value, so go fetch one + // We'll put a temp value in the cache though so we don't + // make a load of requests while we wait for a response + ncNodeNameCache.keys[lookupId] = "Loading..."; - // See if there is a value in the cache and use that - if (ncNodeNameCache.keys[lookupId]) { - return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); - } + // If the service has already been invoked, don't do it again + if (serviceInvoked) { + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } - // No value, so go fetch one - // We'll put a temp value in the cache though so we don't - // make a load of requests while we wait for a response - ncNodeNameCache.keys[lookupId] = "Loading..."; + serviceInvoked = true; - var type = lookupId.indexOf("umb://media/") === 0 - ? "Media" - : lookupId.indexOf("umb://member/") === 0 - ? "Member" - : "Document"; - entityResource.getById(lookupId, type) - .then( - function (ent) { - ncNodeNameCache.keys[lookupId] = ent.name; - } - ); + var udi = udiParser.parse(lookupId); - // Return the current value for now - return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); - }; + if (udi) { + entityResource.getById(udi.value, udi.entityType).then(function (ent) { + ncNodeNameCache.keys[lookupId] = ent.name; + }).catch(function () { + ncNodeNameCache.keys[lookupId] = "Error: Could not load"; + }); + } else { + ncNodeNameCache.keys[lookupId] = "Error: Not a UDI"; + } + + // Return the current value for now + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } + + return nodeNameFilter; }).filter("ncRichText", function () { - return function(input) { - return $("
").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/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; + } + } + }; + } })(); From 748fb7d1f7e8b3b622bab6de95a8e85ff33d6aeb Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 29 Aug 2022 09:50:48 +0200 Subject: [PATCH 27/49] Implement new backoffice installer (#12790) * Add new BackOfficeApi project * Add swagger * Add and route new install controller * Add new install steps * Add Setup endpoint * Add missing RequiresExecution methods * Fix nullability of databasemodel * Move user information to separate model * Remove ping method * Add view models install data * Move mapping folder * Move ViewModels * Add settings endpoint * Remove unused binderprovider * Postfix RequiresExecution with async * Update NewDatabaseUpgradeStep to not depend on install step * Add installstep collection * Move registration into backoffice project * Add InstallService * Use service in controller * Add upgrade to install service and use in controller * Correctly check is database is configured * Reorganize * Reorganize into new core and infrastructure * Rename steps * Rename BackofficeApi to MangementApi * Make install step an interface instead of abstract class * Rename InstallStep to create CreateUserStep * Move restart runtime and sign in user into install steps * Move install service into new core project * Map controllers in composer * Restrict access to installcontroller based on runtime level * Use FireAndForget when logging install * Use actionresult instead of iactionresult * Set new projects as not packable * Link to backoffice in 201 response when installed * Register installations * Add custom backoffice routing template token * Move umbraco path trimming out of application convention * Make it easier to route to backoffice api * Make swagger version aware and move behind backoffice path * Obsolete old install classes * Move maps into single file This is all mappint to/from viewmodels in some manner * Remove usage of InstallSetupResult * Move new projects to the src folder * Remove InstallationType from IInstallStep This upgrade steps should implement their own IUpgradeStep interface * Remove upgrade from service and controller This should be its own service and controller * Add xml docs * Remove internals visible to * Disable package validation for new projects Quite the gotcha here, if the projects are brand new, there is no nuget packages to compare with, this causes the build to fail. * Add ValidateDatabase endpoint * Remove project references to new backoffice We don't actually want to depend on this yet, it's just needed for testing/development * Obsolete installationtype * Add DatabaseSettingsFactory tests * Add InstallServiceTests * Fix InstallServiceTests * Test RequireRuntimeLevelAttribute * Implement new backoffice upgrader (#12818) * Add UpgradeSettingsModel and viewmodel * Add upgrade/settings endpoint * Implement upgrade steps * Add upgrade step collection * Add UpgradeService * Add authorize endpoint to UpgradeController * Fix interface * Add upgrade service tests * Remove runtime check in databaseinstallstep * Move RequireRuntimeLevel to controller * Add a readme to the new backoffice part * BackOffice not Backoffice * Add conditional project references * Fixes based on review * Fix up * Move running of steps into its own method in UpgradeService * Make services transient * More fixup * Log exceptions when running steps --- .github/New BackOffice - README.md | 18 ++ .../Controllers/NewInstallController.cs | 111 +++++++++++ .../Controllers/UpgradeController.cs | 60 ++++++ .../InstallerBuilderExtensions.cs | 79 ++++++++ .../Filters/RequireRuntimeLevelAttribute.cs | 36 ++++ .../ManagementApiComposer.cs | 132 +++++++++++++ .../InstallerViewModelsMapDefinition.cs | 144 ++++++++++++++ .../Umbraco.Cms.ManagementApi.csproj | 29 +++ .../Installer/ConsentLevelViewModel.cs | 16 ++ .../Installer/DatabaseInstallViewModel.cs | 36 ++++ .../Installer/DatabaseSettingsViewModel.cs | 40 ++++ .../Installer/InstallSettingsViewModel.cs | 13 ++ .../ViewModels/Installer/InstallViewModel.cs | 21 ++ .../Installer/UpgradeSettingsViewModel.cs | 23 +++ .../Installer/UserInstallViewModel.cs | 26 +++ .../Installer/UserSettingsViewModel.cs | 16 ++ .../SqlAzureDatabaseProviderMetadata.cs | 7 +- src/Umbraco.Cms/Umbraco.Cms.csproj | 4 + src/Umbraco.Core/Constants-Web.cs | 5 + src/Umbraco.Core/Install/InstallException.cs | 2 +- .../Install/InstallStatusTracker.cs | 3 +- .../InstallSteps/FilePermissionsStep.cs | 3 +- .../InstallSteps/TelemetryIdentifierStep.cs | 3 +- .../Install/InstallSteps/UpgradeStep.cs | 3 +- .../Install/Models/DatabaseModel.cs | 6 +- .../Install/Models/InstallInstructions.cs | 3 +- .../Models/InstallProgressResultModel.cs | 3 +- .../Install/Models/InstallSetup.cs | 3 +- .../Install/Models/InstallSetupResult.cs | 3 +- .../Install/Models/InstallSetupStep.cs | 4 +- .../Models/InstallSetupStepAttribute.cs | 3 +- .../Install/Models/InstallTrackingItem.cs | 3 +- .../Install/Models/InstallationType.cs | 1 + src/Umbraco.Core/Install/Models/Package.cs | 3 +- src/Umbraco.Core/Install/Models/UserModel.cs | 1 + .../Install/InstallStepCollection.cs | 10 +- .../InstallSteps/CompleteInstallStep.cs | 1 + .../InstallSteps/DatabaseConfigureStep.cs | 1 + .../InstallSteps/DatabaseInstallStep.cs | 1 + .../InstallSteps/DatabaseUpgradeStep.cs | 1 + .../Install/InstallSteps/NewInstallStep.cs | 1 + .../Umbraco.Infrastructure.csproj | 4 +- .../Factories/IDatabaseSettingsFactory.cs | 16 ++ .../Factories/IInstallSettingsFactory.cs | 8 + .../Factories/IUpgradeSettingsFactory.cs | 8 + .../Factories/IUserSettingsFactory.cs | 8 + .../Factories/InstallSettingsFactory.cs | 24 +++ .../Factories/UpgradeSettingsFactory.cs | 34 ++++ .../Factories/UserSettingsFactory.cs | 53 ++++++ .../Installer/IInstallStep.cs | 23 +++ .../Installer/IUpgradeStep.cs | 18 ++ .../Installer/NewInstallStepCollection.cs | 12 ++ .../NewInstallStepCollectionBuilder.cs | 11 ++ .../Installer/Steps/FilePermissionsStep.cs | 47 +++++ .../Installer/Steps/RestartRuntimeStep.cs | 23 +++ .../Steps/TelemetryIdentifierStep.cs | 45 +++++ .../Installer/UpgradeStepCollection.cs | 11 ++ .../Installer/UpgradeStepCollectionBuilder.cs | 11 ++ .../Models/Installer/ConsentLevelModel.cs | 10 + .../Models/Installer/DatabaseInstallData.cs | 20 ++ .../Models/Installer/DatabaseSettingsModel.cs | 26 +++ .../Models/Installer/InstallData.cs | 12 ++ .../Models/Installer/InstallSettingsModel.cs | 8 + .../Models/Installer/PasswordSettingsModel.cs | 8 + .../Models/Installer/UpgradeSettingsModel.cs | 15 ++ .../Models/Installer/UserInstallData.cs | 12 ++ .../Models/Installer/UserSettingsModel.cs | 8 + .../Services/Installer/IInstallService.cs | 14 ++ .../Services/Installer/IUpgradeService.cs | 11 ++ .../Services/Installer/InstallService.cs | 61 ++++++ .../Services/Installer/UpgradeService.cs | 61 ++++++ .../Umbraco.New.Cms.Core.csproj | 17 ++ .../Installer/DatabaseSettingsFactory.cs | 54 ++++++ .../Installer/Steps/CreateUserStep.cs | 176 +++++++++++++++++ .../Installer/Steps/DatabaseConfigureStep.cs | 68 +++++++ .../Installer/Steps/DatabaseInstallStep.cs | 49 +++++ .../Installer/Steps/DatabaseUpgradeStep.cs | 79 ++++++++ .../Steps/RegisterInstallCompleteStep.cs | 24 +++ .../Umbraco.New.Cms.Infrastructure.csproj | 18 ++ .../Installer/SignInUserStep.cs | 32 ++++ .../Routing/BackOfficeRouteAttribute.cs | 17 ++ .../Routing/UmbracoBackofficeToken.cs | 42 ++++ .../Umbraco.New.Cms.Web.Common.csproj | 17 ++ .../Install/InstallApiController.cs | 1 + .../Install/InstallAreaRoutes.cs | 2 +- .../Install/InstallController.cs | 1 + .../Umbraco.Web.BackOffice.csproj | 4 + .../RequireRuntimeLevelAttributeTest.cs | 79 ++++++++ .../DistributedCache/DistributedCacheTests.cs | 4 +- ...faultShortStringHelperTestsWithoutSetup.cs | 2 +- .../Services/InstallServiceTests.cs | 111 +++++++++++ .../Services/UpgradeServiceTests.cs | 108 +++++++++++ .../Factories/DatabaseSettingsFactoryTests.cs | 180 ++++++++++++++++++ .../Umbraco.Tests.UnitTests.csproj | 1 + .../Controllers/MemberControllerUnitTests.cs | 2 +- umbraco.sln | 40 ++++ 96 files changed, 2603 insertions(+), 25 deletions(-) create mode 100644 .github/New BackOffice - README.md create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs create mode 100644 src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs create mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs create mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs create mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs create mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs create mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs create mode 100644 src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj create mode 100644 src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs create mode 100644 src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj create mode 100644 src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs create mode 100644 src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs create mode 100644 src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs create mode 100644 src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs diff --git a/.github/New BackOffice - README.md b/.github/New BackOffice - README.md new file mode 100644 index 0000000000..3d04ef2e36 --- /dev/null +++ b/.github/New BackOffice - README.md @@ -0,0 +1,18 @@ +# New Backoffice + +> **Warning**: +> This is an early WIP, and is set not to be packable since we don't want to release this yet. There will be breaking changes in these projects + +This solution folder contains the projects for the new BackOffice. If you're looking to fix or improve the existing CMS, this is not the place to do it, although we do very much appreciate your efforts. + +### Project structure + +Since the new backoffice API is still very much a work in progress we've created new projects for the new backoffice API: + +* Umbrao.Cms.ManagementApi - The "presentation layer" for the management API +* "New" versions of existing projects, should be merged with the existing projects when the new API is released: + * Umbraco.New.Cms.Core + * Umbraco.New.Cms.Infrastructure + * Umbraco.New.Cms.Web.Common + +This also means that we have to use "InternalsVisibleTo" for the new projects since these should be able to access the internal classes since they will when they get merged. diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs new file mode 100644 index 0000000000..94029edad4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/NewInstallController.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Install; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[BackOfficeRoute("api/v{version:apiVersion}/install")] +[RequireRuntimeLevel(RuntimeLevel.Install)] +public class NewInstallController : Controller +{ + private readonly IUmbracoMapper _mapper; + private readonly IInstallSettingsFactory _installSettingsFactory; + private readonly IInstallService _installService; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly InstallHelper _installHelper; + private readonly DatabaseBuilder _databaseBuilder; + + public NewInstallController( + IUmbracoMapper mapper, + IInstallSettingsFactory installSettingsFactory, + IInstallService installService, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + InstallHelper installHelper, + DatabaseBuilder databaseBuilder) + { + _mapper = mapper; + _installSettingsFactory = installSettingsFactory; + _installService = installService; + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; + _installHelper = installHelper; + _databaseBuilder = databaseBuilder; + } + + [HttpGet("settings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(typeof(InstallSettingsViewModel), StatusCodes.Status200OK)] + public async Task> Settings() + { + // Register that the install has started + await _installHelper.SetInstallStatusAsync(false, string.Empty); + + InstallSettingsModel installSettings = _installSettingsFactory.GetInstallSettings(); + InstallSettingsViewModel viewModel = _mapper.Map(installSettings)!; + + return viewModel; + } + + [HttpPost("setup")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Setup(InstallViewModel installData) + { + InstallData data = _mapper.Map(installData)!; + await _installService.Install(data); + + var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); + return Created(backOfficePath, null); + } + + [HttpPost("validateDatabase")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ValidateDatabase(DatabaseInstallViewModel viewModel) + { + // TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async + // We want these to be async for future use (Ideally we'll have more async things), + // But we need to figure out how we want to handle it in the meantime? use Task.FromResult or? + DatabaseModel databaseModel = _mapper.Map(viewModel)!; + + var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true); + + if (success) + { + return Ok(); + } + + var invalidModelProblem = new ProblemDetails + { + Title = "Invalid database configuration", + Detail = "The provided database configuration is invalid", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return BadRequest(invalidModelProblem); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs new file mode 100644 index 0000000000..29164adfb2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/UpgradeController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers; + +// TODO: This needs to be an authorized controller. +[ApiController] +[ApiVersion("1.0")] +[RequireRuntimeLevel(RuntimeLevel.Upgrade)] +[BackOfficeRoute("api/v{version:apiVersion}/upgrade")] +public class UpgradeController : Controller +{ + private readonly IUpgradeSettingsFactory _upgradeSettingsFactory; + private readonly IUpgradeService _upgradeService; + private readonly IUmbracoMapper _mapper; + + public UpgradeController( + IUpgradeSettingsFactory upgradeSettingsFactory, + IUpgradeService upgradeService, + IUmbracoMapper mapper) + { + _upgradeSettingsFactory = upgradeSettingsFactory; + _upgradeService = upgradeService; + _mapper = mapper; + } + + [HttpPost("authorize")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task Authorize() + { + await _upgradeService.Upgrade(); + return Ok(); + } + + [HttpGet("settings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(UpgradeSettingsViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)] + public async Task> Settings() + { + // TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async + // We want these to be async for future use (Ideally we'll have more async things), + // But we need to figure out how we want to handle it in the meantime? use Task.FromResult or? + UpgradeSettingsModel upgradeSettings = _upgradeSettingsFactory.GetUpgradeSettings(); + UpgradeSettingsViewModel viewModel = _mapper.Map(upgradeSettings)!; + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs new file mode 100644 index 0000000000..385cd1ff51 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Installer.Steps; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Infrastructure.Factories.Installer; +using Umbraco.New.Cms.Infrastructure.Installer.Steps; +using Umbraco.New.Cms.Web.Common.Installer; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class InstallerBuilderExtensions +{ + internal static IUmbracoBuilder AddNewInstaller(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + builder.WithCollectionBuilder() + .Add(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + builder.AddInstallSteps(); + services.AddTransient(); + + return builder; + } + + internal static IUmbracoBuilder AddUpgrader(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.AddTransient(); + builder.AddUpgradeSteps(); + services.AddTransient(); + + return builder; + } + + internal static IUmbracoBuilder AddInstallSteps(this IUmbracoBuilder builder) + { + builder.InstallSteps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return builder; + } + + public static NewInstallStepCollectionBuilder InstallSteps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + internal static IUmbracoBuilder AddUpgradeSteps(this IUmbracoBuilder builder) + { + builder.UpgradeSteps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return builder; + } + + public static UpgradeStepCollectionBuilder UpgradeSteps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs new file mode 100644 index 0000000000..0c6dfd8e93 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttribute.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireRuntimeLevelAttribute : ActionFilterAttribute +{ + private readonly RuntimeLevel _requiredRuntimeLevel; + + public RequireRuntimeLevelAttribute(RuntimeLevel requiredRuntimeLevel) => + _requiredRuntimeLevel = requiredRuntimeLevel; + + public override void OnActionExecuting(ActionExecutingContext context) + { + IRuntimeState runtimeState = context.HttpContext.RequestServices.GetRequiredService(); + if (runtimeState.Level == _requiredRuntimeLevel) + { + return; + } + + // We're not in the expected runtime level, so we need to short circuit + var problemDetails = new ProblemDetails + { + Title = "Invalid runtime level", + Detail = $"Runtime level {_requiredRuntimeLevel} is required", + Status = StatusCodes.Status428PreconditionRequired, + Type = "Error", + }; + + context.Result = new ObjectResult(problemDetails) { StatusCode = StatusCodes.Status428PreconditionRequired }; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs new file mode 100644 index 0000000000..f0921f2244 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSwag.AspNetCore; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.ManagementApi.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi; + +public class ManagementApiComposer : IComposer +{ + private const string ApiTitle = "Umbraco Backoffice API"; + private const string ApiAllName = "All"; + + private ApiVersion DefaultApiVersion => new(1, 0); + + public void Compose(IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + builder + .AddNewInstaller() + .AddUpgrader(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = DefaultApiVersion; + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + options.AssumeDefaultVersionWhenUnspecified = true; + options.UseApiBehavior = false; + }); + + services.AddOpenApiDocument(options => + { + options.Title = ApiTitle; + options.Version = ApiAllName; + options.DocumentName = ApiAllName; + options.Description = "This shows all APIs available in this version of Umbraco - Including all the legacy apis that is available for backward compatibility"; + }); + + services.AddVersionedApiExplorer(options => + { + options.DefaultApiVersion = DefaultApiVersion; + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + options.AddApiVersionParametersWhenVersionNeutral = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + // Not super happy with this, but we need to know the UmbracoPath when registering the controller + // To be able to replace the route template token + GlobalSettings? globalSettings = + builder.Config.GetSection(Constants.Configuration.ConfigGlobal).Get(); + var backofficePath = globalSettings.UmbracoPath.TrimStart(Constants.CharArrays.TildeForwardSlash); + + services.AddControllers(options => + { + options.Conventions.Add(new UmbracoBackofficeToken(Constants.Web.AttributeRouting.BackOfficeToken, backofficePath)); + }); + + builder.Services.Configure(options => + { + options.AddFilter(new UmbracoPipelineFilter( + "BackofficeSwagger", + applicationBuilder => + { + applicationBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context => + { + Exception? exception = context.Features.Get()?.Error; + if (exception is null) + { + return; + } + + var response = new ProblemDetails + { + Title = exception.Message, + Detail = exception.StackTrace, + Status = StatusCodes.Status500InternalServerError, + Instance = exception.GetType().Name, + Type = "Error", + }; + await context.Response.WriteAsJsonAsync(response); + })); + }, + applicationBuilder => + { + IServiceProvider provider = applicationBuilder.ApplicationServices; + GlobalSettings? settings = provider.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = provider.GetRequiredService(); + var officePath = settings.GetBackOfficePath(hostingEnvironment); + + // serve documents (same as app.UseSwagger()) + applicationBuilder.UseOpenApi(config => + { + config.Path = $"{officePath}/swagger/{{documentName}}/swagger.json"; + }); + + // Serve Swagger UI + applicationBuilder.UseSwaggerUi3(config => + { + config.Path = officePath + "/swagger"; + config.SwaggerRoutes.Clear(); + var swaggerPath = $"{officePath}/swagger/{ApiAllName}/swagger.json"; + config.SwaggerRoutes.Add(new SwaggerUi3Route(ApiAllName, swaggerPath)); + }); + }, + applicationBuilder => + { + applicationBuilder.UseEndpoints(endpoints => + { + // Maps attribute routed controllers. + endpoints.MapControllers(); + }); + } + )); + }); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs new file mode 100644 index 0000000000..88c50a8715 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Installer/InstallerViewModelsMapDefinition.cs @@ -0,0 +1,144 @@ +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.ManagementApi.ViewModels.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.Cms.ManagementApi.Mapping.Installer; + +public class InstallerViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new InstallData(), Map); + mapper.Define((source, context) => new UserInstallData(), Map); + mapper.Define((source, context) => new DatabaseInstallData(), Map); + mapper.Define((source, context) => new DatabaseModel(), Map); + mapper.Define((source, context) => new DatabaseModel(), Map); + mapper.Define((source, context) => new InstallSettingsViewModel(), Map); + mapper.Define((source, context) => new UserSettingsViewModel(), Map); + mapper.Define((source, context) => new DatabaseSettingsModel(), Map); + mapper.Define((source, context) => new DatabaseSettingsViewModel(), Map); + mapper.Define((source, context) => new ConsentLevelViewModel(), Map); + mapper.Define((source, context) => new UpgradeSettingsViewModel(), Map); + } + + // Umbraco.Code.MapAll + private void Map(UpgradeSettingsModel source, UpgradeSettingsViewModel target, MapperContext context) + { + target.CurrentState = source.CurrentState; + target.NewState = source.NewState; + target.NewVersion = source.NewVersion.ToString(); + target.OldVersion = source.OldVersion.ToString(); + } + + // Umbraco.Code.MapAll + private void Map(DatabaseInstallViewModel source, DatabaseModel target, MapperContext context) + { + target.ConnectionString = source.ConnectionString; + target.DatabaseName = source.Name ?? string.Empty; + target.DatabaseProviderMetadataId = source.Id; + target.IntegratedAuth = source.UseIntegratedAuthentication; + target.Login = source.Username; + target.Password = source.Password; + target.ProviderName = source.ProviderName; + target.Server = source.Server!; + } + + // Umbraco.Code.MapAll + private static void Map(InstallViewModel source, InstallData target, MapperContext context) + { + target.TelemetryLevel = source.TelemetryLevel; + target.User = context.Map(source.User)!; + target.Database = context.Map(source.Database)!; + } + + // Umbraco.Code.MapAll + private static void Map(UserInstallViewModel source, UserInstallData target, MapperContext context) + { + target.Email = source.Email; + target.Name = source.Name; + target.Password = source.Password; + target.SubscribeToNewsletter = source.SubscribeToNewsletter; + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseInstallViewModel source, DatabaseInstallData target, MapperContext context) + { + target.Id = source.Id; + target.ProviderName = source.ProviderName; + target.Server = source.Server; + target.Name = source.Name; + target.Username = source.Username; + target.Password = source.Password; + target.UseIntegratedAuthentication = source.UseIntegratedAuthentication; + target.ConnectionString = source.ConnectionString; + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseInstallData source, DatabaseModel target, MapperContext context) + { + target.ConnectionString = source.ConnectionString; + target.DatabaseName = source.Name ?? string.Empty; + target.DatabaseProviderMetadataId = source.Id; + target.IntegratedAuth = source.UseIntegratedAuthentication; + target.Login = source.Username; + target.Password = source.Password; + target.ProviderName = source.ProviderName; + target.Server = source.Server!; + } + + // Umbraco.Code.MapAll + private static void Map(InstallSettingsModel source, InstallSettingsViewModel target, MapperContext context) + { + target.User = context.Map(source.UserSettings)!; + target.Databases = context.MapEnumerable(source.DatabaseSettings); + } + + // Umbraco.Code.MapAll + private static void Map(UserSettingsModel source, UserSettingsViewModel target, MapperContext context) + { + target.MinCharLength = source.PasswordSettings.MinCharLength; + target.MinNonAlphaNumericLength = source.PasswordSettings.MinNonAlphaNumericLength; + target.ConsentLevels = context.MapEnumerable(source.ConsentLevels); + } + + // Umbraco.Code.MapAll + private static void Map(IDatabaseProviderMetadata source, DatabaseSettingsModel target, MapperContext context) + { + target.DefaultDatabaseName = source.DefaultDatabaseName; + target.DisplayName = source.DisplayName; + target.Id = source.Id; + target.ProviderName = source.ProviderName ?? string.Empty; + target.RequiresConnectionTest = source.RequiresConnectionTest; + target.RequiresCredentials = source.RequiresCredentials; + target.RequiresServer = source.RequiresServer; + target.ServerPlaceholder = source.ServerPlaceholder ?? string.Empty; + target.SortOrder = source.SortOrder; + target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + target.IsConfigured = false; // Defaults to false, we'll set this to true if needed, + } + + // Umbraco.Code.MapAll + private static void Map(DatabaseSettingsModel source, DatabaseSettingsViewModel target, MapperContext context) + { + target.DefaultDatabaseName = source.DefaultDatabaseName; + target.DisplayName = source.DisplayName; + target.Id = source.Id; + target.IsConfigured = source.IsConfigured; + target.ProviderName = source.ProviderName; + target.RequiresConnectionTest = source.RequiresConnectionTest; + target.RequiresCredentials = source.RequiresCredentials; + target.RequiresServer = source.RequiresServer; + target.ServerPlaceholder = source.ServerPlaceholder; + target.SortOrder = source.SortOrder; + target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + } + + // Umbraco.Code.MapAll + private static void Map(ConsentLevelModel source, ConsentLevelViewModel target, MapperContext context) + { + target.Description = source.Description; + target.Level = source.Level; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj new file mode 100644 index 0000000000..fb8c327221 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + nullable + Umbraco.Cms.ManagementApi + false + false + + + + + + + + + + + + + + + + all + + + diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs new file mode 100644 index 0000000000..2774f5ba2e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "consentLevels")] +public class ConsentLevelViewModel +{ + [DataMember(Name = "level")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public TelemetryLevel Level { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs new file mode 100644 index 0000000000..1bc2f4c3e9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "databaseInstall")] +public class DatabaseInstallViewModel +{ + [DataMember(Name = "id")] + [Required] + public Guid Id { get; init; } + + [DataMember(Name = "providerName")] + [Required] + public string? ProviderName { get; init; } + + [DataMember(Name = "server")] + public string? Server { get; init; } + + [DataMember(Name = "name")] + public string? Name { get; init; } + + [DataMember(Name = "username")] + public string? Username { get; init; } + + [DataMember(Name = "password")] + [PasswordPropertyText] + public string? Password { get; init; } + + [DataMember(Name = "useIntegratedAuthentication")] + public bool UseIntegratedAuthentication { get; init; } + + [DataMember(Name = "connectionString")] + public string? ConnectionString { get; init; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs new file mode 100644 index 0000000000..0d2c45f105 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "databaseSettings")] +public class DatabaseSettingsViewModel +{ + [DataMember(Name = "id")] + public Guid Id { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } = string.Empty; + + [DataMember(Name = "defaultDatabaseName")] + public string DefaultDatabaseName { get; set; } = string.Empty; + + [DataMember(Name = "providerName")] + public string ProviderName { get; set; } = string.Empty; + + [DataMember(Name = "isConfigured")] + public bool IsConfigured { get; set; } + + [DataMember(Name = "requiresServer")] + public bool RequiresServer { get; set; } + + [DataMember(Name = "serverPlaceholder")] + public string ServerPlaceholder { get; set; } = string.Empty; + + [DataMember(Name = "requiresCredentials")] + public bool RequiresCredentials { get; set; } + + [DataMember(Name = "supportsIntegratedAuthentication")] + public bool SupportsIntegratedAuthentication { get; set; } + + [DataMember(Name = "requiresConnectionTest")] + public bool RequiresConnectionTest { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs new file mode 100644 index 0000000000..156aa73e3e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "installSettings")] +public class InstallSettingsViewModel +{ + [DataMember(Name = "user")] + public UserSettingsViewModel User { get; set; } = null!; + + [DataMember(Name = "databases")] + public IEnumerable Databases { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs new file mode 100644 index 0000000000..ed815a521d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +public class InstallViewModel +{ + [DataMember(Name = "user")] + [Required] + public UserInstallViewModel User { get; init; } = null!; + + [DataMember(Name = "database")] + [Required] + public DatabaseInstallViewModel Database { get; init; } = null!; + + [DataMember(Name = "telemetryLevel")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public TelemetryLevel TelemetryLevel { get; init; } = TelemetryLevel.Basic; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs new file mode 100644 index 0000000000..8274246070 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "upgradeSettingsViewModel")] +public class UpgradeSettingsViewModel +{ + [DataMember(Name = "currentState")] + public string CurrentState { get; set; } = string.Empty; + + [DataMember(Name = "newState")] + public string NewState { get; set; } = string.Empty; + + [DataMember(Name = "newVersion")] + public string NewVersion { get; set; } = string.Empty; + + [DataMember(Name = "oldVersion")] + public string OldVersion { get; set; } = string.Empty; + + [DataMember(Name = "reportUrl")] + public string ReportUrl => + $"https://our.umbraco.com/contribute/releases/compare?from={OldVersion}&to={NewVersion}¬es=1"; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs new file mode 100644 index 0000000000..dbdb859f63 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +public class UserInstallViewModel +{ + [DataMember(Name = "name")] + [Required] + [StringLength(255)] + public string Name { get; init; } = null!; + + [DataMember(Name = "email")] + [Required] + [EmailAddress] + public string Email { get; init; } = null!; + + [DataMember(Name = "password")] + [Required] + [PasswordPropertyText] + public string Password { get; init; } = null!; + + [DataMember(Name = "subscribeToNewsletter")] + public bool SubscribeToNewsletter { get; init; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs new file mode 100644 index 0000000000..b2be9e88c9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; + +[DataContract(Name = "user")] +public class UserSettingsViewModel +{ + [DataMember(Name = "minCharLength")] + public int MinCharLength { get; set; } + + [DataMember(Name = "minNonAlphaNumericLength")] + public int MinNonAlphaNumericLength { get; set; } + + [DataMember(Name = "consentLevels")] + public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index 0dbc62fb49..112d556712 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -53,6 +53,11 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata /// public string GenerateConnectionString(DatabaseModel databaseModel) { + if (databaseModel.Server is null) + { + throw new ArgumentNullException(nameof(databaseModel.Server)); + } + var server = databaseModel.Server; var databaseName = databaseModel.DatabaseName; var user = databaseModel.Login; @@ -89,7 +94,7 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata server = $"{server},1433"; } - if (user.Contains("@") == false) + if (user?.Contains("@") == false) { var userDomain = server; diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 23e8febd18..c6c63108ff 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -16,6 +16,10 @@ + + + + $(ProjectDir)appsettings-schema.json $(ProjectDir)../JsonSchema/ diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index bfbe4e56d5..bbeae780d8 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -63,6 +63,11 @@ public static partial class Constants public const string AreaToken = "area"; } + public static class AttributeRouting + { + public const string BackOfficeToken = "umbracoBackOffice"; + } + public static class EmailTypes { public const string HealthCheck = "HealthCheck"; diff --git a/src/Umbraco.Core/Install/InstallException.cs b/src/Umbraco.Core/Install/InstallException.cs index 69e28db92c..fcb878c677 100644 --- a/src/Umbraco.Core/Install/InstallException.cs +++ b/src/Umbraco.Core/Install/InstallException.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install; diff --git a/src/Umbraco.Core/Install/InstallStatusTracker.cs b/src/Umbraco.Core/Install/InstallStatusTracker.cs index 5403ded3ae..f1f92ef46c 100644 --- a/src/Umbraco.Core/Install/InstallStatusTracker.cs +++ b/src/Umbraco.Core/Install/InstallStatusTracker.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Collections; +using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Serialization; @@ -9,6 +9,7 @@ namespace Umbraco.Cms.Core.Install; /// /// An internal in-memory status tracker for the current installation /// +[Obsolete("This will no longer be used with the new backoffice APi, instead all steps run in one go")] public class InstallStatusTracker { private static ConcurrentHashSet _steps = new(); diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index 40f54bab33..b6a08d55ae 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Install.Models; @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps; /// /// Represents a step in the installation that ensure all the required permissions on files and folders are correct. /// +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "Permissions", diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs index cb008bf77c..6db33486f5 100644 --- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; @@ -9,6 +9,7 @@ using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "TelemetryIdConfiguration", diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 763b69226e..c67b1fa5fb 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; @@ -8,6 +8,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps /// /// This step is purely here to show the button to commence the upgrade /// + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.Upgrade, "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")] public class UpgradeStep : InstallSetupStep { diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index eb892d9cee..b52fc84fa9 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -11,6 +11,8 @@ public class DatabaseModel [DataMember(Name = "providerName")] public string? ProviderName { get; set; } + // TODO: Make this nullable in V11 + // Server can be null, for instance when installing a SQLite database. [DataMember(Name = "server")] public string Server { get; set; } = null!; @@ -18,10 +20,10 @@ public class DatabaseModel public string DatabaseName { get; set; } = null!; [DataMember(Name = "login")] - public string Login { get; set; } = null!; + public string? Login { get; set; } [DataMember(Name = "password")] - public string Password { get; set; } = null!; + public string? Password { get; set; } [DataMember(Name = "integratedAuth")] public bool IntegratedAuth { get; set; } diff --git a/src/Umbraco.Core/Install/Models/InstallInstructions.cs b/src/Umbraco.Core/Install/Models/InstallInstructions.cs index c86307d9b0..caabf0561c 100644 --- a/src/Umbraco.Core/Install/Models/InstallInstructions.cs +++ b/src/Umbraco.Core/Install/Models/InstallInstructions.cs @@ -1,7 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "installInstructions", Namespace = "")] public class InstallInstructions { diff --git a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs index 650c746998..3b82cac3de 100644 --- a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs +++ b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs @@ -1,10 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; /// /// Returned to the UI for each installation step that is completed /// +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "result", Namespace = "")] public class InstallProgressResultModel { diff --git a/src/Umbraco.Core/Install/Models/InstallSetup.cs b/src/Umbraco.Core/Install/Models/InstallSetup.cs index 2a1e3ce9f7..8b3ce4bb97 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetup.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetup.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.Install.Models; /// Model containing all the install steps for setting up the UI /// [DataContract(Name = "installSetup", Namespace = "")] +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallSetup { public InstallSetup() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs index 3849a09d75..a256a23436 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs @@ -1,8 +1,9 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; /// /// The object returned from each installation step /// +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallSetupResult { public InstallSetupResult() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs index a9d24447c6..2fe3d9814f 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Install.Models; @@ -7,6 +7,7 @@ namespace Umbraco.Cms.Core.Install.Models; /// Model to give to the front-end to collect the information for each step /// [DataContract(Name = "step", Namespace = "")] +[Obsolete("Will be replaced with IInstallStep in the new backoffice API")] public abstract class InstallSetupStep : InstallSetupStep { /// @@ -30,6 +31,7 @@ public abstract class InstallSetupStep : InstallSetupStep } [DataContract(Name = "step", Namespace = "")] +[Obsolete("Will be replaced with IInstallStep in the new backoffice API")] public abstract class InstallSetupStep { protected InstallSetupStep() diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs index c6d0657d33..63edcf0942 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs @@ -1,5 +1,6 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the use of IInstallStep in the new backoffice API")] public sealed class InstallSetupStepAttribute : Attribute { public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) diff --git a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs index 74170857b5..70dc08b39c 100644 --- a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs +++ b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs @@ -1,5 +1,6 @@ -namespace Umbraco.Cms.Core.Install.Models; +namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] public class InstallTrackingItem { public InstallTrackingItem(string name, int serverOrder) diff --git a/src/Umbraco.Core/Install/Models/InstallationType.cs b/src/Umbraco.Core/Install/Models/InstallationType.cs index b2b6a428fa..a2e6c92bad 100644 --- a/src/Umbraco.Core/Install/Models/InstallationType.cs +++ b/src/Umbraco.Core/Install/Models/InstallationType.cs @@ -1,5 +1,6 @@ namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("This will no longer be used with the new backoffice APi, install steps and upgrade steps is instead two different interfaces.")] [Flags] public enum InstallationType { diff --git a/src/Umbraco.Core/Install/Models/Package.cs b/src/Umbraco.Core/Install/Models/Package.cs index 9ac30ab9a7..f85e4b1f67 100644 --- a/src/Umbraco.Core/Install/Models/Package.cs +++ b/src/Umbraco.Core/Install/Models/Package.cs @@ -1,7 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("This is no longer used, instead PackageDefinition and InstalledPackage is used")] [DataContract(Name = "package")] public class Package { diff --git a/src/Umbraco.Core/Install/Models/UserModel.cs b/src/Umbraco.Core/Install/Models/UserModel.cs index 61f76c795d..debae20806 100644 --- a/src/Umbraco.Core/Install/Models/UserModel.cs +++ b/src/Umbraco.Core/Install/Models/UserModel.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Install.Models; +[Obsolete("Will no longer be required with the new backoffice API")] [DataContract(Name = "user", Namespace = "")] public class UserModel { diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 7b711f8750..c07e48705b 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Infrastructure.Install.InstallSteps; namespace Umbraco.Cms.Infrastructure.Install; +[Obsolete("This will be replaced with an ordered collection with the new backoffice")] public sealed class InstallStepCollection { private readonly InstallHelper _installHelper; @@ -17,9 +18,12 @@ public sealed class InstallStepCollection InstallSetupStep[] a = installerSteps.ToArray(); _orderedInstallerSteps = new InstallSetupStep[] { - a.OfType().First(), a.OfType().First(), - a.OfType().First(), a.OfType().First(), - a.OfType().First(), a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), + a.OfType().First(), a.OfType().First(), // TODO: Add these back once we have a compatible Starter kit diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs index d212909a9f..c67d1f64b0 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Install.Models; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep( InstallationType.NewInstall | InstallationType.Upgrade, "UmbracoVersion", diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 87be3c6e8f..8d2886c223 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -9,6 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] public class DatabaseConfigureStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 42712f20bd..f328fea676 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; +[Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] public class DatabaseInstallStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index fa35ee5b07..4039533fa1 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -13,6 +13,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, "DatabaseUpgrade", 12, "")] public class DatabaseUpgradeStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 2ebc756dc2..cf984aed59 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -29,6 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps /// error, etc... and the end-user refreshes the installer then we cannot show the user screen because they've already entered that information so instead we'll /// display a simple continue installation view. /// + [Obsolete("Will be replace with a new step with the new backoffice")] [InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] public class NewInstallStep : InstallSetupStep { diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 7d48468a96..93219af4c9 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -16,7 +16,6 @@ TRACE_SCOPES; - @@ -111,6 +110,9 @@ <_Parameter1>DynamicProxyGenAssembly2 + + <_Parameter1>Umbraco.New.Cms.Infrastructure + diff --git a/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs new file mode 100644 index 0000000000..c71ce126d8 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs @@ -0,0 +1,16 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +/// +/// Creates based on the currently configured providers. +/// +public interface IDatabaseSettingsFactory +{ + /// + /// Creates a collection of database settings models for the currently installed database providers + /// + /// Collection of database settings. + /// Thrown if a connection string is preconfigured, but provider name is missing. + ICollection GetDatabaseSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs new file mode 100644 index 0000000000..552cd0af2b --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IInstallSettingsFactory +{ + InstallSettingsModel GetInstallSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs new file mode 100644 index 0000000000..45daf3dcc1 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IUpgradeSettingsFactory +{ + UpgradeSettingsModel GetUpgradeSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs new file mode 100644 index 0000000000..5c069d7084 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs @@ -0,0 +1,8 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IUserSettingsFactory +{ + UserSettingsModel GetUserSettings(); +} diff --git a/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs new file mode 100644 index 0000000000..5d57b1554d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs @@ -0,0 +1,24 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class InstallSettingsFactory : IInstallSettingsFactory +{ + private readonly IUserSettingsFactory _userSettingsFactory; + private readonly IDatabaseSettingsFactory _databaseSettingsFactory; + + public InstallSettingsFactory( + IUserSettingsFactory userSettingsFactory, + IDatabaseSettingsFactory databaseSettingsFactory) + { + _userSettingsFactory = userSettingsFactory; + _databaseSettingsFactory = databaseSettingsFactory; + } + + public InstallSettingsModel GetInstallSettings() => + new() + { + DatabaseSettings = _databaseSettingsFactory.GetDatabaseSettings(), + UserSettings = _userSettingsFactory.GetUserSettings(), + }; +} diff --git a/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs new file mode 100644 index 0000000000..314e83a995 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs @@ -0,0 +1,34 @@ +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class UpgradeSettingsFactory : IUpgradeSettingsFactory +{ + private readonly IRuntimeState _runtimeState; + private readonly IUmbracoVersion _umbracoVersion; + + public UpgradeSettingsFactory( + IRuntimeState runtimeState, + IUmbracoVersion umbracoVersion) + { + _runtimeState = runtimeState; + _umbracoVersion = umbracoVersion; + } + + + public UpgradeSettingsModel GetUpgradeSettings() + { + var model = new UpgradeSettingsModel + { + CurrentState = _runtimeState.CurrentMigrationState ?? string.Empty, + NewState = _runtimeState.FinalMigrationState ?? string.Empty, + NewVersion = _umbracoVersion.SemanticVersion, + OldVersion = new SemVersion(_umbracoVersion.SemanticVersion.Major), // TODO can we find the old version somehow? e.g. from current state + }; + + return model; + } +} diff --git a/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs new file mode 100644 index 0000000000..9386cf713a --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Factories; + +public class UserSettingsFactory : IUserSettingsFactory +{ + private readonly ILocalizedTextService _localizedTextService; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + + public UserSettingsFactory( + IOptions securitySettings, + ILocalizedTextService localizedTextService) + { + _localizedTextService = localizedTextService; + _passwordConfiguration = securitySettings.Value; + } + + public UserSettingsModel GetUserSettings() => + new() + { + PasswordSettings = CreatePasswordSettingsModel(), + ConsentLevels = CreateConsentLevelModels(), + }; + + private PasswordSettingsModel CreatePasswordSettingsModel() => + new() + { + MinCharLength = _passwordConfiguration.RequiredLength, + MinNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars() + }; + + private IEnumerable CreateConsentLevelModels() => + Enum.GetValues() + .ToList() + .Select(level => new ConsentLevelModel + { + Level = level, + Description = GetTelemetryLevelDescription(level), + }); + + private string GetTelemetryLevelDescription(TelemetryLevel telemetryLevel) => telemetryLevel switch + { + TelemetryLevel.Minimal => _localizedTextService.Localize("analytics", "minimalLevelDescription"), + TelemetryLevel.Basic => _localizedTextService.Localize("analytics", "basicLevelDescription"), + TelemetryLevel.Detailed => _localizedTextService.Localize("analytics", "detailedLevelDescription"), + _ => throw new ArgumentOutOfRangeException(nameof(telemetryLevel), $"Did not expect telemetry level of {telemetryLevel}") + }; +} diff --git a/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs b/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs new file mode 100644 index 0000000000..3344b9ffb0 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs @@ -0,0 +1,23 @@ +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer; + +/// +/// Defines a step that's required to install Umbraco. +/// +public interface IInstallStep +{ + /// + /// Executes the install step. + /// + /// InstallData model containing the data provided by the installer UI. + /// + Task ExecuteAsync(InstallData model); + + /// + /// Determines if the step is required to execute. + /// + /// InstallData model containing the data provided by the installer UI. + /// True if the step should execute, otherwise false. + Task RequiresExecutionAsync(InstallData model); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs b/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs new file mode 100644 index 0000000000..6f52aca6ec --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs @@ -0,0 +1,18 @@ +namespace Umbraco.New.Cms.Core.Installer; + +/// +/// Defines a step that's required to upgrade Umbraco. +/// +public interface IUpgradeStep +{ + /// + /// Executes the upgrade step. + /// + Task ExecuteAsync(); + + /// + /// Determines if the step is required to execute. + /// + /// True if the step should execute, otherwise false. + Task RequiresExecutionAsync(); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs new file mode 100644 index 0000000000..7077b85a28 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.New.Cms.Core.Installer; + +public class NewInstallStepCollection : BuilderCollectionBase +{ + public NewInstallStepCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs new file mode 100644 index 0000000000..d3c572b7b7 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class NewInstallStepCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override NewInstallStepCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs new file mode 100644 index 0000000000..37574c91e1 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs @@ -0,0 +1,47 @@ +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class FilePermissionsStep : IInstallStep, IUpgradeStep +{ + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _localizedTextService; + + public FilePermissionsStep( + IFilePermissionHelper filePermissionHelper, + ILocalizedTextService localizedTextService) + { + _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + // validate file permissions + var permissionsOk = + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> report); + + var translatedErrors = + report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); + if (permissionsOk == false) + { + throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private static Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs new file mode 100644 index 0000000000..cacce0d763 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class RestartRuntimeStep : IInstallStep, IUpgradeStep +{ + private readonly IRuntime _runtime; + + public RestartRuntimeStep(IRuntime runtime) => _runtime = runtime; + + public async Task ExecuteAsync(InstallData _) => await Execute(); + + public async Task ExecuteAsync() => await Execute(); + + private async Task Execute() => await _runtime.RestartAsync(); + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs new file mode 100644 index 0000000000..a05a8228fe --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Installer.Steps; + +public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep +{ + private readonly IOptions _globalSettings; + private readonly ISiteIdentifierService _siteIdentifierService; + + public TelemetryIdentifierStep( + IOptions globalSettings, + ISiteIdentifierService siteIdentifierService) + { + _globalSettings = globalSettings; + _siteIdentifierService = siteIdentifierService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + _siteIdentifierService.TryCreateSiteIdentifier(out _); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + { + // Verify that Json value is not empty string + // Try & get a value stored in appSettings.json + var backofficeIdentifierRaw = _globalSettings.Value.Id; + + // No need to add Id again if already found + return Task.FromResult(string.IsNullOrEmpty(backofficeIdentifierRaw)); + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs new file mode 100644 index 0000000000..1deb06681d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class UpgradeStepCollection : BuilderCollectionBase +{ + public UpgradeStepCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs new file mode 100644 index 0000000000..a7b2b803ce --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.New.Cms.Core.Installer; + +public class UpgradeStepCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override UpgradeStepCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs new file mode 100644 index 0000000000..a3687814c3 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class ConsentLevelModel +{ + public TelemetryLevel Level { get; set; } + + public string Description { get; set; } = string.Empty; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs new file mode 100644 index 0000000000..6141ea7a9f --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs @@ -0,0 +1,20 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class DatabaseInstallData +{ + public Guid Id { get; set; } + + public string? ProviderName { get; set; } + + public string? Server { get; set; } + + public string? Name { get; set; } + + public string? Username { get; set; } + + public string? Password { get; set; } + + public bool UseIntegratedAuthentication { get; set; } + + public string? ConnectionString { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs new file mode 100644 index 0000000000..2f8aabb8af --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs @@ -0,0 +1,26 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class DatabaseSettingsModel +{ + public Guid Id { get; set; } + + public int SortOrder { get; set; } + + public string DisplayName { get; set; } = string.Empty; + + public string DefaultDatabaseName { get; set; } = string.Empty; + + public string ProviderName { get; set; } = string.Empty; + + public bool IsConfigured { get; set; } + + public bool RequiresServer { get; set; } + + public string ServerPlaceholder { get; set; } = string.Empty; + + public bool RequiresCredentials { get; set; } + + public bool SupportsIntegratedAuthentication { get; set; } + + public bool RequiresConnectionTest { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs new file mode 100644 index 0000000000..2283cf2482 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class InstallData +{ + public UserInstallData User { get; set; } = null!; + + public DatabaseInstallData Database { get; set; } = null!; + + public TelemetryLevel TelemetryLevel { get; set; } = TelemetryLevel.Basic; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs new file mode 100644 index 0000000000..6b0aeb370d --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class InstallSettingsModel +{ + public UserSettingsModel UserSettings { get; set; } = null!; + + public ICollection DatabaseSettings { get; set; } = new List(); +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs new file mode 100644 index 0000000000..2efec3a696 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class PasswordSettingsModel +{ + public int MinCharLength { get; set; } + + public int MinNonAlphaNumericLength { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs new file mode 100644 index 0000000000..b403367548 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Semver; +using Umbraco.Extensions; + +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UpgradeSettingsModel +{ + public string CurrentState { get; set; } = string.Empty; + + public string NewState { get; set; } = string.Empty; + + public SemVersion NewVersion { get; set; } = null!; + + public SemVersion OldVersion { get; set; } = null!; +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs new file mode 100644 index 0000000000..18865565df --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs @@ -0,0 +1,12 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UserInstallData +{ + public string Name { get; set; } = null!; + + public string Email { get; set; } = null!; + + public string Password { get; set; } = null!; + + public bool SubscribeToNewsletter { get; set; } +} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs new file mode 100644 index 0000000000..2db9f04b65 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.New.Cms.Core.Models.Installer; + +public class UserSettingsModel +{ + public PasswordSettingsModel PasswordSettings { get; set; } = null!; + + public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs new file mode 100644 index 0000000000..c5dc499d62 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs @@ -0,0 +1,14 @@ +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public interface IInstallService +{ + /// + /// Runs all the steps in the , installing Umbraco + /// + /// InstallData containing the required data used to install + /// + Task Install(InstallData model); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs new file mode 100644 index 0000000000..f6eefa8a85 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs @@ -0,0 +1,11 @@ +using Umbraco.New.Cms.Core.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public interface IUpgradeService +{ + /// + /// Runs all the steps in the , upgrading Umbraco. + /// + Task Upgrade(); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs new file mode 100644 index 0000000000..98813cdaec --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public class InstallService : IInstallService +{ + private readonly ILogger _logger; + private readonly NewInstallStepCollection _installSteps; + private readonly IRuntimeState _runtimeState; + + public InstallService( + ILogger logger, + NewInstallStepCollection installSteps, + IRuntimeState runtimeState) + { + _logger = logger; + _installSteps = installSteps; + _runtimeState = runtimeState; + } + + /// + public async Task Install(InstallData model) + { + if (_runtimeState.Level != RuntimeLevel.Install) + { + throw new InvalidOperationException($"Runtime level must be Install to install but was: {_runtimeState.Level}"); + } + + try + { + await RunSteps(model); + } + catch (Exception exception) + { + _logger.LogError(exception, "Encountered an error when running the install steps"); + throw; + } + } + + private async Task RunSteps(InstallData model) + { + foreach (IInstallStep step in _installSteps) + { + var stepName = step.GetType().Name; + _logger.LogInformation("Checking if {StepName} requires execution", stepName); + if (await step.RequiresExecutionAsync(model) is false) + { + _logger.LogInformation("Skipping {StepName}", stepName); + continue; + } + + _logger.LogInformation("Running {StepName}", stepName); + await step.ExecuteAsync(model); + _logger.LogInformation("Finished {StepName}", stepName); + } + } +} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs new file mode 100644 index 0000000000..6f11e8a7ac --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public class UpgradeService : IUpgradeService +{ + private readonly UpgradeStepCollection _upgradeSteps; + private readonly IRuntimeState _runtimeState; + private readonly ILogger _logger; + + public UpgradeService( + UpgradeStepCollection upgradeSteps, + IRuntimeState runtimeState, + ILogger logger) + { + _upgradeSteps = upgradeSteps; + _runtimeState = runtimeState; + _logger = logger; + } + + /// + public async Task Upgrade() + { + if (_runtimeState.Level != RuntimeLevel.Upgrade) + { + throw new InvalidOperationException( + $"Runtime level must be Upgrade to upgrade but was: {_runtimeState.Level}"); + } + + try + { + await RunSteps(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Encountered an error when running the upgrade steps"); + throw; + } + } + + private async Task RunSteps() + { + foreach (IUpgradeStep step in _upgradeSteps) + { + var stepName = step.GetType().Name; + _logger.LogInformation("Checking if {StepName} requires execution", stepName); + if (await step.RequiresExecutionAsync() is false) + { + _logger.LogInformation("Skipping {StepName}", stepName); + continue; + } + + _logger.LogInformation("Running {StepName}", stepName); + await step.ExecuteAsync(); + _logger.LogInformation("Finished {StepName}", stepName); + } + } +} diff --git a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj new file mode 100644 index 0000000000..e7c38a23af --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + Umbraco.New.Cms.Core + false + nullable + false + + + + + + + diff --git a/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs new file mode 100644 index 0000000000..be941104b6 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Factories; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Factories.Installer; + +public class DatabaseSettingsFactory : IDatabaseSettingsFactory +{ + private readonly IEnumerable _databaseProviderMetadata; + private readonly IOptionsMonitor _connectionStrings; + private readonly IUmbracoMapper _mapper; + + public DatabaseSettingsFactory( + IEnumerable databaseProviderMetadata, + IOptionsMonitor connectionStrings, + IUmbracoMapper mapper) + { + _databaseProviderMetadata = databaseProviderMetadata; + _connectionStrings = connectionStrings; + _mapper = mapper; + } + + /// + public ICollection GetDatabaseSettings() + { + ConnectionStrings? connectionString = _connectionStrings.CurrentValue; + + // If the connection string is configured we just return the configured provider. + if (connectionString.IsConnectionStringConfigured()) + { + var providerName = connectionString.ProviderName; + IDatabaseProviderMetadata? providerMetaData = _databaseProviderMetadata + .FirstOrDefault(x => x.ProviderName?.Equals(providerName, StringComparison.InvariantCultureIgnoreCase) ?? false); + + if (providerMetaData is null) + { + throw new InvalidOperationException($"Provider {providerName} is not a registered provider"); + } + + DatabaseSettingsModel configuredProvider = _mapper.Map(providerMetaData)!; + + configuredProvider.IsConfigured = true; + + return new[] { configuredProvider }; + } + + List providers = _mapper.MapEnumerable(_databaseProviderMetadata); + return providers; + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs new file mode 100644 index 0000000000..38faddff09 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs @@ -0,0 +1,176 @@ +using System.Collections.Specialized; +using System.Data.Common; +using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; +using Constants = Umbraco.Cms.Core.Constants; +using HttpResponseMessage = System.Net.Http.HttpResponseMessage; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class CreateUserStep : IInstallStep +{ + private readonly IUserService _userService; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IHttpClientFactory _httpClientFactory; + private readonly SecuritySettings _securitySettings; + private readonly IOptionsMonitor _connectionStrings; + private readonly ICookieManager _cookieManager; + private readonly IBackOfficeUserManager _userManager; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IMetricsConsentService _metricsConsentService; + + public CreateUserStep( + IUserService userService, + DatabaseBuilder databaseBuilder, + IHttpClientFactory httpClientFactory, + IOptions securitySettings, + IOptionsMonitor connectionStrings, + ICookieManager cookieManager, + IBackOfficeUserManager userManager, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IMetricsConsentService metricsConsentService) + { + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _httpClientFactory = httpClientFactory; + _securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings)); + _connectionStrings = connectionStrings; + _cookieManager = cookieManager; + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _metricsConsentService = metricsConsentService; + } + + public async Task ExecuteAsync(InstallData model) + { + IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } + + UserInstallData user = model.User; + admin.Email = user.Email.Trim(); + admin.Name = user.Name.Trim(); + admin.Username = user.Email.Trim(); + + _userService.Save(admin); + + BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + { + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } + + IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } + + _metricsConsentService.SetConsentLevel(model.TelemetryLevel); + + if (model.User.SubscribeToNewsletter) + { + var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } }; + var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); + + HttpClient httpClient = _httpClientFactory.CreateClient(); + + try + { + HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; + } + catch { /* fail in silence */ } + } + } + + /// + public Task RequiresExecutionAsync(InstallData model) + { + InstallState installState = GetInstallState(); + if (installState.HasFlag(InstallState.Unknown)) + { + // In this one case when it's a brand new install and nothing has been configured, make sure the + // back office cookie is cleared so there's no old cookies lying around causing problems + _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); + } + + var shouldRun = installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.HasNonDefaultUser); + return Task.FromResult(shouldRun); + } + + private InstallState GetInstallState() + { + InstallState installState = InstallState.Unknown; + + if (_databaseBuilder.IsDatabaseConfigured) + { + installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; + } + + ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; + + var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); + if (isConnectionStringConfigured) + { + installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; + } + + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); + var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); + if (isConnectionAvailable) + { + installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; + } + + var isUmbracoInstalled = isConnectionAvailable && _databaseBuilder.IsUmbracoInstalled(); + if (isUmbracoInstalled) + { + installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; + } + + var hasSomeNonDefaultUser = isUmbracoInstalled && _databaseBuilder.HasSomeNonDefaultUser(); + if (hasSomeNonDefaultUser) + { + installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; + } + + return installState; + } + + [Flags] + private enum InstallState : short + { + // This is an easy way to avoid 0 enum assignment and not worry about + // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags + Unknown = 1, + HasVersion = 1 << 1, + HasConnectionString = 1 << 2, + ConnectionStringConfigured = 1 << 3, + CanConnect = 1 << 4, + UmbracoInstalled = 1 << 5, + HasNonDefaultUser = 1 << 6 + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs new file mode 100644 index 0000000000..dd78e149e5 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseConfigureStep : IInstallStep +{ + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly ILogger _logger; + private readonly IUmbracoMapper _mapper; + + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IUmbracoMapper mapper) + { + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _logger = logger; + _mapper = mapper; + } + + public Task ExecuteAsync(InstallData model) + { + DatabaseModel databaseModel = _mapper.Map(model.Database)!; + + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseModel, false)) + { + throw new InstallException("Could not connect to the database"); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) + { + // If the connection string is already present in config we don't need to configure it again + if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) + { + try + { + // Since a connection string was present we verify the db can connect and query + _databaseBuilder.ValidateSchema(); + + return Task.FromResult(false); + } + catch (Exception ex) + { + // Something went wrong, could not connect so probably need to reconfigure + _logger.LogError(ex, "An error occurred, reconfiguring..."); + + return Task.FromResult(true); + } + } + + return Task.FromResult(true); + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs new file mode 100644 index 0000000000..9abe6823ab --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseInstallStep : IInstallStep, IUpgradeStep +{ + private readonly IRuntimeState _runtime; + private readonly DatabaseBuilder _databaseBuilder; + + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) + { + _runtime = runtime; + _databaseBuilder = databaseBuilder; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + + DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); + + if (result?.Success == false) + { + throw new InstallException("The database failed to install. ERROR: " + result.Message); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs new file mode 100644 index 0000000000..83cae8d80b --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep +{ + private readonly DatabaseBuilder _databaseBuilder; + private readonly IRuntimeState _runtime; + private readonly ILogger _logger; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IKeyValueService _keyValueService; + + public DatabaseUpgradeStep( + DatabaseBuilder databaseBuilder, + IRuntimeState runtime, + ILogger logger, + IUmbracoVersion umbracoVersion, + IKeyValueService keyValueService) + { + _databaseBuilder = databaseBuilder; + _runtime = runtime; + _logger = logger; + _umbracoVersion = umbracoVersion; + _keyValueService = keyValueService; + } + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() + { + _logger.LogInformation("Running 'Upgrade' service"); + + var plan = new UmbracoPlan(_umbracoVersion); + plan.AddPostMigration(); // needed when running installer (back-office) + + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + + if (result?.Success == false) + { + throw new InstallException("The database failed to upgrade. ERROR: " + result.Message); + } + + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private Task ShouldExecute() + { + // Don't do anything if RunTimeLevel is not Install/Upgrade + if (_runtime.Level == RuntimeLevel.Run) + { + return Task.FromResult(false); + } + + // Check the upgrade state, if it matches we dont have to upgrade. + var plan = new UmbracoPlan(_umbracoVersion); + var currentState = _keyValueService.GetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name); + if (currentState != plan.FinalState) + { + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs new file mode 100644 index 0000000000..53989bf3b7 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Infrastructure.Install; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; + +public class RegisterInstallCompleteStep : IInstallStep, IUpgradeStep +{ + private readonly InstallHelper _installHelper; + + public RegisterInstallCompleteStep(InstallHelper installHelper) => _installHelper = installHelper; + + public Task ExecuteAsync(InstallData _) => Execute(); + + public Task ExecuteAsync() => Execute(); + + private Task Execute() => _installHelper.SetInstallStatusAsync(true, string.Empty); + + public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); + + public Task RequiresExecutionAsync() => ShouldExecute(); + + private static Task ShouldExecute() => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj new file mode 100644 index 0000000000..27e27cc981 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + Umbraco.New.Cms.Infrastructure + false + nullable + false + + + + + + + + diff --git a/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs new file mode 100644 index 0000000000..62954c61e0 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; + +namespace Umbraco.New.Cms.Web.Common.Installer; + +public class SignInUserStep : IInstallStep +{ + private readonly IBackOfficeSignInManager _backOfficeSignInManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; + + public SignInUserStep( + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager) + { + _backOfficeSignInManager = backOfficeSignInManager; + _backOfficeUserManager = backOfficeUserManager; + } + + public InstallationType InstallationTypeTarget => InstallationType.NewInstall; + + public async Task ExecuteAsync(InstallData model) + { + BackOfficeIdentityUser identityUser = await _backOfficeUserManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + await _backOfficeSignInManager.SignInAsync(identityUser, false); + } + + public Task RequiresExecutionAsync(InstallData model) => Task.FromResult(true); +} diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs b/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs new file mode 100644 index 0000000000..44b1c27e2d --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/BackOfficeRouteAttribute.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; + +namespace Umbraco.New.Cms.Web.Common.Routing; + +/// +/// Routes a controller within the backoffice area, I.E /umbraco +/// +public class BackOfficeRouteAttribute : RouteAttribute +{ + // All this does is append [umbracoBackoffice]/ to the route, + // this is then replaced with whatever is configures as UmbracoPath by the UmbracoBackofficeToken convention + public BackOfficeRouteAttribute(string template) + : base($"[{Constants.Web.AttributeRouting.BackOfficeToken}]/" + template.TrimStart('/')) + { + } +} diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs b/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs new file mode 100644 index 0000000000..b8e5e45c00 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/UmbracoBackofficeToken.cs @@ -0,0 +1,42 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Umbraco.New.Cms.Web.Common.Routing; + +/// +/// Adds a custom template token for specifying backoffice route with attribute routing +/// +// Adapted from https://stackoverflow.com/questions/68911881/asp-net-core-api-add-custom-route-token-resolver +public class UmbracoBackofficeToken : IApplicationModelConvention +{ + private readonly string _umbracoPath; + private readonly string _tokenRegex; + + public UmbracoBackofficeToken(string tokenName, string umbracoPath) + { + _umbracoPath = umbracoPath; + _tokenRegex = $@"(\[{tokenName}])(? actionModel.Selectors), _umbracoPath); + } + } + + private void UpdateSelectors(IEnumerable selectors, string tokenValue) + { + foreach (SelectorModel selector in selectors.Where(s => s.AttributeRouteModel is not null)) + { + // We just checked that AttributeRouteModel is not null, so silence the nullable warning + selector.AttributeRouteModel!.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue); + selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue); + } + } + + private string? InsertTokenValue(string? template, string tokenValue) + => template is null ? template : Regex.Replace(template, _tokenRegex, tokenValue); +} diff --git a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj new file mode 100644 index 0000000000..159537c9d8 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + false + nullable + false + + + + + + + + diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index 3dfc9f51b1..e5f1c4fc36 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -18,6 +18,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; +[Obsolete("Will be replaced with a new API controller in the new backoffice api")] [UmbracoApiController] [AngularJsonOnlyConfiguration] [InstallAuthorize] diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs index 26eb3e9302..590fb73e0e 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs @@ -8,6 +8,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; +[Obsolete("Will be replaced with attribute routing in the new backoffice API")] public class InstallAreaRoutes : IAreaRoutes { private readonly IHostingEnvironment _hostingEnvironment; @@ -40,7 +41,6 @@ public class InstallAreaRoutes : IAreaRoutes ControllerExtensions.GetControllerName(), Constants.Web.Mvc.InstallArea); - break; case RuntimeLevel.Run: diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index ab6029cc43..c8af0d8ba8 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install; /// /// The Installation controller /// +[Obsolete("Will no longer be required with the new backoffice API")] [InstallAuthorize] [Area(Constants.Web.Mvc.InstallArea)] public class InstallController : Controller diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 3f24e2717e..eb3a5c5f01 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -49,6 +49,10 @@ + + + + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs new file mode 100644 index 0000000000..9bf039af66 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.ManagementApi/Filters/RequireRuntimeLevelAttributeTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Filters; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.ManagementApi.Filters; + +[TestFixture] +public class RequireRuntimeLevelAttributeTest +{ + [Test] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Run, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Unknown, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Boot, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Upgrade, true)] + [TestCase(RuntimeLevel.Run, RuntimeLevel.Upgrade, true)] + [TestCase(RuntimeLevel.Install, RuntimeLevel.Install, false)] + [TestCase(RuntimeLevel.Upgrade, RuntimeLevel.Upgrade, false)] + public void BlocksWhenIncorrectRuntime(RuntimeLevel requiredLevel, RuntimeLevel actualLevel, bool shouldFail) + { + var executionContext = CreateActionExecutingContext(actualLevel); + + var sut = new RequireRuntimeLevelAttribute(requiredLevel); + sut.OnActionExecuting(executionContext); + + if (shouldFail) + { + AssertFailure(executionContext); + return; + } + + // Assert success, result being null == we haven't short circuited. + Assert.IsNull(executionContext.Result); + } + + private void AssertFailure(ActionExecutingContext executionContext) + { + var result = executionContext.Result; + Assert.IsInstanceOf(result); + + var objectResult = (ObjectResult)result; + + Assert.AreEqual(StatusCodes.Status428PreconditionRequired, objectResult?.StatusCode); + Assert.IsInstanceOf(objectResult?.Value); + } + + private ActionExecutingContext CreateActionExecutingContext(RuntimeLevel targetRuntimeLevel) + { + var actionContext = new ActionContext() + { + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + ActionDescriptor = new ActionDescriptor() + }; + + var executingContext = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + new()); + + var fakeRuntime = new Mock(); + fakeRuntime.Setup(x => x.Level).Returns(targetRuntimeLevel); + + var fakeServiceProvider = new Mock(); + fakeServiceProvider.Setup(x => x.GetService(typeof(IRuntimeState))).Returns(fakeRuntime.Object); + actionContext.HttpContext.RequestServices = fakeServiceProvider.Object; + + return executingContext; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs index 53270a5ac2..104831f025 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DistributedCache/DistributedCacheTests.cs @@ -24,10 +24,10 @@ public class DistributedCacheTests var cacheRefresherCollection = new CacheRefresherCollection(() => new[] { new TestCacheRefresher() }); - _distributedCache = new Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection); + _distributedCache = new global::Umbraco.Cms.Core.Cache.DistributedCache(ServerMessenger, cacheRefresherCollection); } - private Cms.Core.Cache.DistributedCache _distributedCache; + private global::Umbraco.Cms.Core.Cache.DistributedCache _distributedCache; private IServerRoleAccessor ServerRegistrar { get; set; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs index 47fcce82ee..a9748a5484 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs @@ -346,7 +346,7 @@ public class DefaultShortStringHelperTestsWithoutSetup public void Utf8ToAsciiConverter() { const string str = "a\U00010F00z\uA74Ftéô"; - var output = Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str); + var output = global::Umbraco.Cms.Core.Strings.Utf8ToAsciiConverter.ToAsciiString(str); Assert.AreEqual("a?zooteo", output); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs new file mode 100644 index 0000000000..cd40293bfd --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/InstallServiceTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Core.Services.Installer; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services; + +[TestFixture] +public class InstallServiceTests +{ + [Test] + public void RequiresInstallRuntimeToInstall() + { + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); + var stepCollection = new NewInstallStepCollection(Enumerable.Empty); + + var sut = new InstallService(Mock.Of>(), stepCollection, runtimeStateMock.Object); + + Assert.ThrowsAsync(async () => await sut.Install(new InstallData())); + } + + [Test] + public async Task OnlyRunsStepsThatRequireExecution() + { + var steps = new[] + { + new TestInstallStep { ShouldRun = true }, + new TestInstallStep { ShouldRun = false }, + new TestInstallStep { ShouldRun = true }, + }; + + var sut = CreateInstallService(steps); + await sut.Install(new InstallData()); + + foreach (var step in steps) + { + Assert.AreEqual(step.ShouldRun, step.HasRun); + } + } + + [Test] + public async Task StepsRunInCollectionOrder() + { + List runOrder = new List(); + + var steps = new[] + { + new TestInstallStep { Id = 1 }, + new TestInstallStep { Id = 2 }, + new TestInstallStep { Id = 3 }, + }; + + // Add an method delegate that will add the step itself, that way we can know the executed order. + foreach (var step in steps) + { + step.AdditionalExecution = _ => + { + runOrder.Add(step); + return Task.CompletedTask; + }; + } + + var sut = CreateInstallService(steps); + await sut.Install(new InstallData()); + + // The ID's are strictly not necessary, but it makes potential debugging easier. + var expectedRunOrder = steps.Select(x => x.Id); + var actualRunOrder = runOrder.Select(x => x.Id); + Assert.AreEqual(expectedRunOrder, actualRunOrder); + } + + private InstallService CreateInstallService(IEnumerable steps) + { + var logger = Mock.Of>(); + var stepCollection = new NewInstallStepCollection(() => steps); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Install); + + return new InstallService(logger, stepCollection, runtimeStateMock.Object); + } + + private class TestInstallStep : IInstallStep + { + public bool HasRun; + + public bool ShouldRun = true; + + public int Id; + + public Func AdditionalExecution; + + public Task ExecuteAsync(InstallData model) + { + HasRun = true; + + AdditionalExecution?.Invoke(model); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync(InstallData model) => Task.FromResult(ShouldRun); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs new file mode 100644 index 0000000000..d2934f9c81 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Core/Services/UpgradeServiceTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Installer; +using UpgradeService = Umbraco.New.Cms.Core.Services.Installer.UpgradeService; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Core.Services; + +[TestFixture] +public class UpgradeServiceTests +{ + + [Test] + [TestCase(RuntimeLevel.Install)] + [TestCase(RuntimeLevel.Boot)] + [TestCase(RuntimeLevel.Run)] + [TestCase(RuntimeLevel.Unknown)] + public void RequiresUpgradeRuntimeToUpgrade(RuntimeLevel level) + { + var sut = CreateUpgradeService(Enumerable.Empty(), level); + + Assert.ThrowsAsync(async () => await sut.Upgrade()); + } + + [Test] + public async Task OnlyRunsStepsThatRequireExecution() + { + var steps = new[] + { + new TestUpgradeStep { ShouldRun = true }, + new TestUpgradeStep { ShouldRun = false }, + new TestUpgradeStep { ShouldRun = true }, + }; + + var sut = CreateUpgradeService(steps); + + await sut.Upgrade(); + + foreach (var step in steps) + { + Assert.AreEqual(step.ShouldRun, step.HasRun); + } + } + + [Test] + public async Task StepsRunInCollectionOrder() + { + List runOrder = new List(); + + var steps = new[] + { + new TestUpgradeStep { Id = 1 }, + new TestUpgradeStep { Id = 2 }, + new TestUpgradeStep { Id = 3 }, + }; + + // Add an method delegate that will add the step itself, that way we can know the executed order. + foreach (var step in steps) + { + step.AdditionalExecution = () => runOrder.Add(step); + } + + var sut = CreateUpgradeService(steps); + await sut.Upgrade(); + + // The ID's are strictly not necessary, but it makes potential debugging easier. + var expectedRunOrder = steps.Select(x => x.Id); + var actualRunOrder = runOrder.Select(x => x.Id); + Assert.AreEqual(expectedRunOrder, actualRunOrder); + } + + private UpgradeService CreateUpgradeService(IEnumerable steps, RuntimeLevel runtimeLevel = RuntimeLevel.Upgrade) + { + var logger = Mock.Of>(); + var stepCollection = new UpgradeStepCollection(() => steps); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(runtimeLevel); + + return new UpgradeService(stepCollection, runtimeStateMock.Object, logger); + } + + private class TestUpgradeStep : IUpgradeStep + { + public bool HasRun; + + public bool ShouldRun = true; + + public int Id; + + public Action AdditionalExecution; + + public Task ExecuteAsync() + { + HasRun = true; + + AdditionalExecution?.Invoke(); + return Task.CompletedTask; + } + + public Task RequiresExecutionAsync() => Task.FromResult(ShouldRun); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs new file mode 100644 index 0000000000..d718daba50 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.Cms.Tests.Common; +using Umbraco.New.Cms.Core.Models.Installer; +using Umbraco.New.Cms.Infrastructure.Factories.Installer; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.New.Cms.Infrastructure.Factories; + +[TestFixture] +public class DatabaseSettingsFactoryTests +{ + [Test] + public void CanBuildDatabaseSettings() + { + var metadata = CreateTestMetadata(); + var connectionString = new TestOptionsMonitor(new ConnectionStrings()); + var mapper = CreateMapper(); + + var factory = new DatabaseSettingsFactory(metadata, connectionString, mapper); + + var settingsModels = factory.GetDatabaseSettings(); + Assert.AreEqual(metadata.Count, settingsModels.Count); + AssertMapping(metadata, settingsModels); + } + + [Test] + public void IsConfiguredSetCorrectly() + { + var connectionString = new ConnectionStrings + { + ConnectionString = "SomeConnectionString", + ProviderName = "HostedTestMeta", + }; + var optionsMonitor = new TestOptionsMonitor(connectionString); + var mapper = CreateMapper(); + var metadata = CreateTestMetadata(); + + var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper); + + var settingsModels = factory.GetDatabaseSettings(); + + Assert.AreEqual(1, settingsModels.Count, "Expected only one database settings model, if a database is preconfigured we should only return the configured one."); + AssertMapping(metadata, settingsModels); + Assert.IsTrue(settingsModels.First().IsConfigured); + } + + [Test] + public void SpecifiedProviderMustExist() + { + var connectionString = new ConnectionStrings + { + ConnectionString = "SomeConnectionString", + ProviderName = "NoneExistentProvider", + }; + var optionsMonitor = new TestOptionsMonitor(connectionString); + var mapper = CreateMapper(); + var metadata = CreateTestMetadata(); + + var factory = new DatabaseSettingsFactory(metadata, optionsMonitor, mapper); + Assert.Throws(() => factory.GetDatabaseSettings()); + } + + /// + /// Asserts that the mapping is correct, in other words that the values in DatabaseSettingsModel is as expected. + /// + private void AssertMapping( + IEnumerable expected, + ICollection actual) + { + expected = expected.ToList(); + foreach (var model in actual) + { + var metadata = expected.FirstOrDefault(x => x.Id == model.Id); + Assert.IsNotNull(metadata); + + Assert.Multiple(() => + { + Assert.AreEqual(metadata?.SortOrder, model.SortOrder); + Assert.AreEqual(metadata.DisplayName, model.DisplayName); + Assert.AreEqual(metadata.DefaultDatabaseName, model.DefaultDatabaseName); + Assert.AreEqual(metadata.ProviderName ?? string.Empty, model.ProviderName); + Assert.AreEqual(metadata.RequiresServer, model.RequiresServer); + Assert.AreEqual(metadata.ServerPlaceholder ?? string.Empty, model.ServerPlaceholder); + Assert.AreEqual(metadata.RequiresCredentials, model.RequiresCredentials); + Assert.AreEqual(metadata.SupportsIntegratedAuthentication, model.SupportsIntegratedAuthentication); + Assert.AreEqual(metadata.RequiresConnectionTest, model.RequiresConnectionTest); + }); + } + } + + private IUmbracoMapper CreateMapper() + { + var mapper = new UmbracoMapper( + new MapDefinitionCollection(Enumerable.Empty), + Mock.Of()); + + var definition = new InstallerViewModelsMapDefinition(); + definition.DefineMaps(mapper); + return mapper; + } + + private List CreateTestMetadata() + { + + var metadata = new List + { + new TestDatabaseProviderMetadata + { + Id = Guid.Parse("EC8ACD63-8CDE-4CA5-B2A3-06322720F274"), + SortOrder = 1, + DisplayName = "FirstMetadata", + DefaultDatabaseName = "TestDatabase", + IsAvailable = true, + GenerateConnectionStringDelegate = _ => "FirstTestMetadataConnectionString", + ProviderName = "SimpleTestMeta" + }, + new TestDatabaseProviderMetadata + { + Id = Guid.Parse("C5AB4E1D-B7E4-47E5-B1A4-C9343B5F59CA"), + SortOrder = 2, + DisplayName = "SecondMetadata", + DefaultDatabaseName = "HostedTest", + IsAvailable = true, + RequiresServer = true, + ServerPlaceholder = "SomeServerPlaceholder", + RequiresCredentials = true, + RequiresConnectionTest = true, + ForceCreateDatabase = true, + GenerateConnectionStringDelegate = _ => "HostedDatabaseConnectionString", + ProviderName = "HostedTestMeta" + }, + }; + + return metadata; + } + + #nullable enable + public class TestDatabaseProviderMetadata : IDatabaseProviderMetadata + { + public Guid Id { get; set; } + + public int SortOrder { get; set; } + + public string DisplayName { get; set; } = string.Empty; + + public string DefaultDatabaseName { get; set; } = string.Empty; + + public string? ProviderName { get; set; } + + public bool SupportsQuickInstall { get; set; } + + public bool IsAvailable { get; set; } + + public bool RequiresServer { get; set; } + + public string? ServerPlaceholder { get; set; } + + public bool RequiresCredentials { get; set; } + + public bool SupportsIntegratedAuthentication { get; set; } + + public bool RequiresConnectionTest { get; set; } + + public bool ForceCreateDatabase { get; set; } + + public Func GenerateConnectionStringDelegate { get; set; } = + _ => "ConnectionString"; + + public string? GenerateConnectionString(DatabaseModel databaseModel) => GenerateConnectionStringDelegate(databaseModel); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 55d875d1bc..33f9d1884c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 10fb0bbb36..c3fc359cf5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -562,7 +562,7 @@ public class MemberControllerUnitTests var map = new MapDefinitionCollection(() => new List { - new Cms.Core.Models.Mapping.MemberMapDefinition(), + new global::Umbraco.Cms.Core.Models.Mapping.MemberMapDefinition(), memberMapDefinition, new ContentTypeMapDefinition( commonMapper, diff --git a/umbraco.sln b/umbraco.sln index 3b172779d2..e0ff14115c 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -96,6 +96,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Templates", "templa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms", "src\Umbraco.Cms\Umbraco.Cms.csproj", "{92EAA57A-CC99-4F5D-9D9C-B865293F6000}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NewBackoffice", "NewBackoffice", "{995D9EFA-8BB1-4333-80AD-C525A06FD984}" + ProjectSection(SolutionItems) = preProject + .github\New BackOffice - README.md = .github\New BackOffice - README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.ManagementApi", "src\Umbraco.Cms.ManagementApi\Umbraco.Cms.ManagementApi.csproj", "{0946531B-F06D-415B-A4E3-6CBFF5DB1C12}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Core", "src\Umbraco.New.Cms.Core\Umbraco.New.Cms.Core.csproj", "{CBCE0A1E-BF29-49A6-9581-EAB3587D823A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Infrastructure", "src\Umbraco.New.Cms.Infrastructure\Umbraco.New.Cms.Infrastructure.csproj", "{2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.New.Cms.Web.Common", "src\Umbraco.New.Cms.Web.Common\Umbraco.New.Cms.Web.Common.csproj", "{5ED13EC6-399E-49D5-9D26-86501729B08D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{D4C3395A-BA9D-4032-9ED3-09F1FC032CBC}" ProjectSection(SolutionItems) = preProject build\csharp-docs\docfx.filter.yml = build\csharp-docs\docfx.filter.yml @@ -243,6 +255,30 @@ Global {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.Release|Any CPU.Build.0 = Release|Any CPU {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {92EAA57A-CC99-4F5D-9D9C-B865293F6000}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.Release|Any CPU.Build.0 = Release|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.Release|Any CPU.Build.0 = Release|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.Release|Any CPU.Build.0 = Release|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.Release|Any CPU.Build.0 = Release|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {5ED13EC6-399E-49D5-9D26-86501729B08D}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -255,6 +291,10 @@ Global {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {05C1D0C8-C592-468F-AF8F-A299B9B3A903} = {6D72A60B-0542-4AA9-A493-DD4179E838A1} + {0946531B-F06D-415B-A4E3-6CBFF5DB1C12} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {CBCE0A1E-BF29-49A6-9581-EAB3587D823A} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {2D978DAF-8F48-4D59-8FEA-7EF0F40DBC2C} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {5ED13EC6-399E-49D5-9D26-86501729B08D} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} {D4C3395A-BA9D-4032-9ED3-09F1FC032CBC} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} {5FBDD50D-7A86-4F4D-BEB9-7967FBA4ED2C} = {D4C3395A-BA9D-4032-9ED3-09F1FC032CBC} {55B028A8-6294-46A4-BED5-7888ADB92368} = {5FBDD50D-7A86-4F4D-BEB9-7967FBA4ED2C} From 9bd6500bc76f463d339ebc3fd6953c4fe1ddc8f5 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 29 Aug 2022 13:23:49 +0200 Subject: [PATCH 28/49] Grammar (do -> does) and some extra text --- .../Migrations/Expressions/Alter/Table/AlterTableBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index fd9dee6745..b4f7b12563 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -26,7 +26,7 @@ public class AlterTableBuilder : ExpressionBuilderBase Date: Tue, 23 Aug 2022 04:01:49 +0530 Subject: [PATCH 29/49] Fix #12770 (#12865) * change "umbraco" to "Umbraco" in translation files. * Replace "" and "" tag with "" and "" (cherry picked from commit fa8bb3f1da34cd8508f64cd86bee91ae626eeb00) --- .../EmbeddedResources/Lang/cs.xml | 12 ++++---- .../EmbeddedResources/Lang/cy.xml | 20 ++++++------- .../EmbeddedResources/Lang/da.xml | 6 ++-- .../EmbeddedResources/Lang/en.xml | 20 ++++++------- .../EmbeddedResources/Lang/en_us.xml | 30 +++++++++---------- .../EmbeddedResources/Lang/es.xml | 2 +- .../EmbeddedResources/Lang/fr.xml | 8 ++--- .../EmbeddedResources/Lang/he.xml | 2 +- .../EmbeddedResources/Lang/it.xml | 16 +++++----- .../EmbeddedResources/Lang/ja.xml | 8 ++--- .../EmbeddedResources/Lang/ko.xml | 2 +- .../EmbeddedResources/Lang/nb.xml | 2 +- .../EmbeddedResources/Lang/nl.xml | 12 ++++---- .../EmbeddedResources/Lang/pt.xml | 2 +- .../EmbeddedResources/Lang/sv.xml | 2 +- .../EmbeddedResources/Lang/tr.xml | 12 ++++---- .../EmbeddedResources/Lang/zh.xml | 2 +- .../EmbeddedResources/Lang/zh_tw.xml | 4 +-- 18 files changed, 81 insertions(+), 81 deletions(-) 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 9a44528b53..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. @@ -2717,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.

]]>
@@ -2731,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.

]]>
@@ -2741,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.

]]> @@ -2910,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的使用權!

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

    不需更多的操作步驟。點選下一步繼續。]]> 密碼已更改 作為入門者,從視頻教程開始吧! 安裝失敗。 From 5fc6fa58c0dd5d175a8462c111db8de493fc2d0f Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 30 Aug 2022 13:22:59 +0200 Subject: [PATCH 30/49] Revert "Break word for limited width content" This reverts commit 448836ee2df5f1e35d94515c7f6d544801b7fe2a. (cherry picked from commit 07fba1eb8413118e7eebd7db1a55c290424fef70) --- .../src/less/components/umb-node-preview.less | 1 + .../src/less/components/umb-readonlyvalue.less | 5 ++--- src/Umbraco.Web.UI.Client/src/less/mixins.less | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) 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-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/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 From 44122c65099c3f465e44bb15af7b23f0df9e7469 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 31 Aug 2022 11:03:34 +0200 Subject: [PATCH 31/49] Performance improvement: Reusable data editors (#12921) * Introduce opt-in option for reusable data editors * Verified RTE as reusable * Make attribute property naming more explicit + update comments * Test file upload and image cropper * Add unit tests --- .../ContentPickerPropertyEditor.cs | 3 +- .../PropertyEditors/DataEditor.cs | 20 +-- .../PropertyEditors/DataEditorAttribute.cs | 6 + .../PropertyEditors/DecimalPropertyEditor.cs | 3 +- .../EyeDropperColorPickerPropertyEditor.cs | 3 +- .../PropertyEditors/IntegerPropertyEditor.cs | 3 +- .../PropertyEditors/LabelPropertyEditor.cs | 3 +- .../PropertyEditors/MarkdownPropertyEditor.cs | 3 +- .../MemberGroupPickerPropertyEditor.cs | 3 +- .../MemberPickerPropertyEditor.cs | 3 +- .../UserPickerPropertyEditor.cs | 3 +- .../UmbracoBuilder.Services.cs | 2 +- .../BlockListPropertyEditor.cs | 3 +- .../CheckBoxListPropertyEditor.cs | 3 +- .../ColorPickerPropertyEditor.cs | 3 +- .../PropertyEditors/DateTimePropertyEditor.cs | 3 +- .../DropDownFlexiblePropertyEditor.cs | 3 +- .../EmailAddressPropertyEditor.cs | 3 +- .../FileUploadPropertyEditor.cs | 3 +- .../PropertyEditors/GridPropertyEditor.cs | 3 +- .../ImageCropperPropertyEditor.cs | 3 +- .../PropertyEditors/ListViewPropertyEditor.cs | 3 +- .../MediaPicker3PropertyEditor.cs | 3 +- .../MediaPickerPropertyEditor.cs | 3 +- .../MultiNodeTreePickerPropertyEditor.cs | 3 +- .../MultiUrlPickerPropertyEditor.cs | 3 +- .../MultipleTextStringPropertyEditor.cs | 3 +- .../NestedContentPropertyEditor.cs | 3 +- .../RadioButtonsPropertyEditor.cs | 3 +- .../PropertyEditors/RichTextPropertyEditor.cs | 3 +- .../PropertyEditors/SliderPropertyEditor.cs | 3 +- .../PropertyEditors/TagsPropertyEditor.cs | 3 +- .../PropertyEditors/TextAreaPropertyEditor.cs | 3 +- .../PropertyEditors/TextboxPropertyEditor.cs | 3 +- .../TrueFalsePropertyEditor.cs | 3 +- .../DataValueEditorReuseTests.cs | 132 ++++++++++++++++++ 36 files changed, 213 insertions(+), 43 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs 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.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/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/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)); + } +} From 2cef2430567b2459915a1beab14a0002757851cc Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 31 Aug 2022 13:24:16 +0200 Subject: [PATCH 32/49] Add virtual SetUpTestConfiguration method (#12928) --- .../Testing/UmbracoIntegrationTest.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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); + } + } } From 87ced87ddd1a1a729950f2bacef99364ebb4a53a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 31 Aug 2022 13:30:40 +0200 Subject: [PATCH 33/49] remove styling from installer (#12923) This styling is now being imported from components/umb-range-slider.less and is no longer needed here Previously fixed in 52672d2c2ba046f1da431388ec95b21b30965f08 Ref https://github.com/umbraco/Umbraco-CMS/pull/12807/files/52672d2c2ba046f1da431388ec95b21b30965f08#r957799952 --- .../src/less/installer.less | 45 ------------------- 1 file changed, 45 deletions(-) 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; - } -} From dc709c20ce13a6a89ca5c16d17fa4af99a5d4045 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 31 Aug 2022 13:30:40 +0200 Subject: [PATCH 34/49] remove styling from installer (#12923) This styling is now being imported from components/umb-range-slider.less and is no longer needed here Previously fixed in 52672d2c2ba046f1da431388ec95b21b30965f08 Ref https://github.com/umbraco/Umbraco-CMS/pull/12807/files/52672d2c2ba046f1da431388ec95b21b30965f08#r957799952 --- .../src/less/installer.less | 45 ------------------- 1 file changed, 45 deletions(-) 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; - } -} From 9cf7e965a962665f88e669badd77bb4b75c08889 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 1 Sep 2022 14:01:49 +0200 Subject: [PATCH 35/49] Backoffice Api: Server Controller (#12932) * Add initial implementation using 1 controller pr. action * Add OpenApiTag attribute Otherwise the endpoints won't be grouped correctly * Use correct response type * Move ApiVersion to endpoint controllers * Add ServerController suffix to endpoint controllers --- .../Server/ServerControllerBase.cs | 13 +++++++++++ .../Server/StatusServerController.cs | 21 ++++++++++++++++++ .../Server/VersionServerController.cs | 22 +++++++++++++++++++ .../Server/ServerStatusViewModel.cs | 10 +++++++++ .../ViewModels/Server/VersionViewModel.cs | 6 +++++ 5 files changed, 72 insertions(+) create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Server/ServerStatusViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Server/VersionViewModel.cs 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/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!; +} From 89d1d7cd9c701aa8eb68fcee22b7eb079e7ce273 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 2 Sep 2022 14:20:02 +0200 Subject: [PATCH 36/49] Use staticserviceprovider to provide service (#12939) Co-authored-by: Zeegaan --- src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index cab595e00f..e1550971e1 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; @@ -14,8 +16,9 @@ public class DictionaryMapDefinition : IMapDefinition private readonly ILocalizationService _localizationService; [Obsolete("Use the constructor with the CommonMapper")] - public DictionaryMapDefinition(ILocalizationService localizationService) => - _localizationService = localizationService; + public DictionaryMapDefinition(ILocalizationService localizationService) : this(localizationService, StaticServiceProvider.Instance.GetRequiredService()) + { + } public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) { From 67dbc6af267a7ea36f1e0f810947811b2101a5b9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 5 Sep 2022 21:05:59 +0200 Subject: [PATCH 37/49] Bump Version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index ec2a88ea63..9ea16ab589 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.1.0", + "version": "10.1.1", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 7b00c0d3e9e3da88f7f2b9cc8f88df28851e5d76 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 5 Sep 2022 21:11:17 +0200 Subject: [PATCH 38/49] Limit what API does when not in used in Install level --- .../Install/InstallApiController.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index 3dfc9f51b1..2bac71504f 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -86,11 +86,15 @@ 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); - _backOfficeSignInManager.SignInAsync(identityUser, false); + if (levelBeforeRestart == RuntimeLevel.Install) + { + BackOfficeIdentityUser identityUser = await _backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserIdAsString); + _backOfficeSignInManager.SignInAsync(identityUser, false); + } return NoContent(); } From 9a2ead4381a60ca0cf2c812f5653eab461129bf0 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:07:47 +0200 Subject: [PATCH 39/49] Add PagedViewModel (#12955) Co-authored-by: Zeegaan --- .../ViewModels/Pagination/PagedViewModel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs 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(); +} From e09dc75e4f422f38221568dc0d4d9b931923adf0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 5 Sep 2022 21:11:17 +0200 Subject: [PATCH 40/49] Limit what API does when not in used in Install level --- .../Install/InstallApiController.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index 3dfc9f51b1..2bac71504f 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -86,11 +86,15 @@ 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); - _backOfficeSignInManager.SignInAsync(identityUser, false); + if (levelBeforeRestart == RuntimeLevel.Install) + { + BackOfficeIdentityUser identityUser = await _backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserIdAsString); + _backOfficeSignInManager.SignInAsync(identityUser, false); + } return NoContent(); } From a83bb2f7677dca2938c85b734bdd53aabe0862e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Wed, 7 Sep 2022 09:19:22 +0200 Subject: [PATCH 41/49] Bump version to non-rc --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 9988b6b4bd..0f9ad08c15 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.2.0-rc", + "version": "10.2.0", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From d038f860bd1e8d0394bfef630cb70ee893407da7 Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Wed, 7 Sep 2022 09:20:08 +0200 Subject: [PATCH 42/49] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 9988b6b4bd..1b7f726e3f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.2.0-rc", + "version": "10.3.0-rc", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From da24ae9180f2198af80cca889d97bf1a7fee8b7d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 7 Sep 2022 11:24:41 +0200 Subject: [PATCH 43/49] Fixed InstallAuthorizeAttribute and simplified other things --- .../Controllers/BackOfficeController.cs | 7 +++ .../Install/InstallApiController.cs | 47 +++++++-------- .../Install/InstallAreaRoutes.cs | 15 ++--- .../Install/InstallAuthorizeAttribute.cs | 56 ++++++++++-------- .../Install/InstallController.cs | 59 +------------------ 5 files changed, 65 insertions(+), 119 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index beee83cbb4..c5567d1796 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/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index 2bac71504f..a454d46e55 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -59,26 +59,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; } @@ -99,9 +100,6 @@ public class InstallApiController : ControllerBase return NoContent(); } - /// - /// Installs. - /// public async Task> PostPerformInstall(InstallInstructions installModel) { if (installModel == null) @@ -109,14 +107,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) { @@ -143,14 +141,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) { @@ -251,8 +250,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; @@ -280,8 +278,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..fe5cd09230 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs @@ -28,22 +28,15 @@ 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..b8f31a9f8f 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; @@ -13,12 +11,10 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.Install; -using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install; - /// /// The Installation controller /// @@ -26,8 +22,6 @@ namespace Umbraco.Cms.Web.BackOffice.Install; [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 +56,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); @@ -96,34 +71,4 @@ 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); - } } From 2ad6633d478ad4dde1912af25fbc8d2509ad299b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 7 Sep 2022 14:08:33 +0200 Subject: [PATCH 44/49] Fix tree root font being larger than it used to be --- .../src/less/components/tree/umb-tree-root.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; From 3846c75cc6d30930cbde9cf46b66d383ce116f5d Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 7 Sep 2022 14:38:54 +0200 Subject: [PATCH 45/49] Hotfix: Move allow edit invariant from non default setting to content settings (#12960) * Use ContentSettings instead of SecuritySettings for AllowEditInvariantFromNonDefault * Make it backwards compatible --- .../Configuration/Models/ContentSettings.cs | 7 ++ .../Configuration/Models/SecuritySettings.cs | 11 +-- .../UmbracoBuilder.Configuration.cs | 15 ++++ .../DependencyInjection/UmbracoBuilder.cs | 2 +- .../Models/Mapping/ContentVariantMapper.cs | 29 ++++++-- .../Services/CultureImpactFactory.cs | 22 ++++-- .../Controllers/BackOfficeServerVariables.cs | 2 +- .../Umbraco.Core/Models/CultureImpactTests.cs | 70 ++++++++++--------- 8 files changed, 105 insertions(+), 53 deletions(-) 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/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 90e2e49c94..6efd096c68 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; @@ -104,6 +105,20 @@ public static partial class UmbracoBuilderExtensions builder.Services.Configure(options => options.MergeReplacements(builder.Config)); + // 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 ff2d4a1f1e..4bfe7fd7bd 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -324,7 +324,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/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/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.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 15deef1ad2..6471b5b2ae 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/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)); + } } From 82934838cc73caec4b6948a07b202695b1cf1340 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:43:26 +0200 Subject: [PATCH 46/49] Extend error message with possible solution (#12962) Co-authored-by: Zeegaan --- src/Umbraco.PublishedCache.NuCache/ContentCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5bb38b39c9aba2240b9fab056c672f7fcccd384c Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Wed, 7 Sep 2022 14:44:47 +0200 Subject: [PATCH 47/49] Undo breaking change --- src/Umbraco.Web.BackOffice/Install/InstallController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index b8f31a9f8f..7c768357a1 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.Install; +using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; @@ -71,4 +72,8 @@ public class InstallController : Controller return View(Path.Combine(Constants.SystemDirectories.Umbraco.TrimStart("~"), Constants.Web.Mvc.InstallArea, nameof(Index) + ".cshtml")); } + + [HttpGet] + [IgnoreFromNotFoundSelectorPolicy] + public ActionResult Redirect() => NotFound(); } From 4683c4a0730a9f04df56a978add349dd6a3898dd Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 7 Sep 2022 14:38:54 +0200 Subject: [PATCH 48/49] Hotfix: Move allow edit invariant from non default setting to content settings (#12960) * Use ContentSettings instead of SecuritySettings for AllowEditInvariantFromNonDefault * Make it backwards compatible --- .../Configuration/Models/ContentSettings.cs | 7 ++ .../Configuration/Models/SecuritySettings.cs | 11 +-- .../UmbracoBuilder.Configuration.cs | 15 ++++ .../DependencyInjection/UmbracoBuilder.cs | 2 +- .../Models/Mapping/ContentVariantMapper.cs | 29 ++++++-- .../Services/CultureImpactFactory.cs | 22 ++++-- .../Controllers/BackOfficeServerVariables.cs | 2 +- .../Umbraco.Core/Models/CultureImpactTests.cs | 70 ++++++++++--------- 8 files changed, 105 insertions(+), 53 deletions(-) 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/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 90e2e49c94..6efd096c68 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; @@ -104,6 +105,20 @@ public static partial class UmbracoBuilderExtensions builder.Services.Configure(options => options.MergeReplacements(builder.Config)); + // 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 ff2d4a1f1e..4bfe7fd7bd 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -324,7 +324,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/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/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.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 15deef1ad2..6471b5b2ae 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/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)); + } } From ae516b28db2dc5256ef037b6cf6d57740cb5214f Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Thu, 8 Sep 2022 08:46:01 +0200 Subject: [PATCH 49/49] Ensure new projects use the correct apssettings key for AllowEditInvariantFromNonDefault --- templates/UmbracoProject/appsettings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 } } }