From a69499a13664fac9e5976825fc37690e51d2c373 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 16 Dec 2021 09:10:55 +0100 Subject: [PATCH 01/23] Bump versions --- .../UmbracoPackage/.template.config/template.json | 2 +- .../UmbracoProject/.template.config/template.json | 2 +- src/Directory.Build.props | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index b69b63dd1c..1a4dd16fd7 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.2.0-rc", + "defaultValue": "9.3.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index 6ec0babd70..fd41de8d1c 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -57,7 +57,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.2.0-rc", + "defaultValue": "9.3.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fed70283e7..995c8afebd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,10 @@ - 9.2.0 - 9.2.0 - 9.2.0-rc - 9.2.0 + 9.3.0 + 9.3.0 + 9.3.0-rc + 9.3.0 9.0 en-US Umbraco CMS From 0ba51f87eaf086bbbf79815ae8a3a25c24106a94 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 20 Dec 2021 08:45:11 +0100 Subject: [PATCH 02/23] Move member properties to Member Content App (V9 merge regression) (#11768) * Fix regression after merging to v9 * Update test to align with removed member properties --- .../Mapping/MemberTabsAndPropertiesMapper.cs | 34 +++++-------------- .../Controllers/MemberControllerUnitTests.cs | 14 -------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index d61e32d88a..d8ac8d635d 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -65,14 +65,11 @@ namespace Umbraco.Cms.Core.Models.Mapping var resolved = base.Map(source, context); - // This is kind of a hack because a developer is supposed to be allowed to set their property editor - would have been much easier - // if we just had all of the membership provider fields on the member table :( - // TODO: But is there a way to map the IMember.IsLockedOut to the property ? i dunno. + // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) var isLockedOutProperty = resolved.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut); if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { - isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); + isLockedOutProperty.Readonly = true; } return resolved; @@ -191,20 +188,6 @@ namespace Umbraco.Cms.Core.Models.Mapping { var properties = new List { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = _localizedTextService.Localize("general","id"), - Value = new List {member.Id.ToString(), member.Key.ToString()}, - View = "idwithguid" - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = _localizedTextService.Localize("content","membertype"), - Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), - View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View - }, GetLoginProperty(member, _localizedTextService), new ContentPropertyDisplay { @@ -212,7 +195,7 @@ namespace Umbraco.Cms.Core.Models.Mapping Label = _localizedTextService.Localize("general","email"), Value = member.Email, View = "email", - Validation = {Mandatory = true} + Validation = { Mandatory = true } }, new ContentPropertyDisplay { @@ -221,12 +204,10 @@ namespace Umbraco.Cms.Core.Models.Mapping Value = new Dictionary { // TODO: why ignoreCase, what are we doing here?! - {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, + { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } }, - // TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor View = "changepassword", - // Initialize the dictionary with the configuration from the default membership provider - Config = GetPasswordConfig(member) + Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider }, new ContentPropertyDisplay { @@ -234,7 +215,10 @@ namespace Umbraco.Cms.Core.Models.Mapping Label = _localizedTextService.Localize("content","membergroup"), Value = GetMemberGroupValue(member.Username), View = "membergroups", - Config = new Dictionary {{"IsRequired", true}} + Config = new Dictionary + { + { "IsRequired", true } + } } }; 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 da5175f272..069e94f732 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -618,20 +618,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Id = 77, Properties = new List() { - new ContentPropertyDisplay() - { - Alias = "_umb_id", - View = "idwithguid", - Value = new [] - { - "123", - "guid" - } - }, - new ContentPropertyDisplay() - { - Alias = "_umb_doctype" - }, new ContentPropertyDisplay() { Alias = "_umb_login" From 2b2e703e30014006bab7dd1f7d76aec7a81f2e55 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Dec 2021 08:57:28 +0100 Subject: [PATCH 03/23] Implement #11776 for v9 --- .../Security/UmbracoApplicationUrlCheck.cs | 68 +++++++++++++++++++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 + .../umbraco/config/lang/en_us.xml | 2 + 3 files changed, 72 insertions(+) create mode 100644 src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs new file mode 100644 index 0000000000..44b10ba0e3 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -0,0 +1,68 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +{ + [HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] + public class UmbracoApplicationUrlCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IOptionsMonitor _webRoutingSettings; + + public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) + { + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } + + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var success = false; + + if (url.IsNullOrWhiteSpace()) + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + success = true; + } + + return new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck + }; + } + } +} diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 468a0cc735..4411209cd5 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -2290,6 +2290,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index aef05ca973..5a17eafbb2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -2372,6 +2372,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> From cc8ea0e8ebb5154d369ea59b9b27fe2b8f54390b Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Dec 2021 08:58:13 +0100 Subject: [PATCH 04/23] Adding UmbracoApplicationUrlCheck "Read more" URL --- src/Umbraco.Core/Constants-HealthChecks.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 5770bd07e4..5a8ea401cb 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { /// /// Defines constants. @@ -20,15 +20,16 @@ public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; } + public static class Configuration { public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; } + public static class FolderAndFilePermissionsCheck { - public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; @@ -37,7 +38,7 @@ public static class Security { - + public const string UmbracoApplicationUrlCheck = "https://umbra.co/healthchecks-umbraco-application-url"; public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; @@ -46,7 +47,6 @@ public static class HttpsCheck { - public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; From 4240f89534a0c123e12407e9bdfd7c76c4c2f602 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 21 Dec 2021 11:15:57 +0100 Subject: [PATCH 05/23] Adds migration back for v9 for #10450 (#11769) --- .../Migrations/Upgrade/UmbracoPlan.cs | 2 +- .../V_9_2_0/AddDefaultForNotificationsToggle.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 257fee9967..ff9e86335f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -267,7 +267,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // TO 9.2.0 To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); - + To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs new file mode 100644 index 0000000000..3bc62ab42e --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +{ + public class AddDefaultForNotificationsToggle : MigrationBase + { + public AddDefaultForNotificationsToggle(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() + { + var updateSQL = Sql($"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); + Execute.Sql(updateSQL.SQL).Do(); + } + } +} From 72d9f56478c996eda346fe48e25631a0fc8afa5f Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Dec 2021 12:48:35 +0100 Subject: [PATCH 06/23] V9: Use current request for emails (#11778) * Use request url for email * Fixed potential null ref exceptions Co-authored-by: Bjarke Berg --- .../Controllers/AuthenticationController.cs | 56 ++++++++++- .../Controllers/UsersController.cs | 92 ++++++++++++++++--- .../Extensions/HttpRequestExtensions.cs | 31 ++++++- 3 files changed, 160 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index f159011d80..30ad3f75ae 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -29,6 +30,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.Models; using Umbraco.Extensions; @@ -71,9 +73,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly LinkGenerator _linkGenerator; private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebRoutingSettings _webRoutingSettings; // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here - + [ActivatorUtilitiesConstructor] public AuthenticationController( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IBackOfficeUserManager backOfficeUserManager, @@ -91,7 +95,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHostingEnvironment hostingEnvironment, LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalAuthenticationOptions, - IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + IHttpContextAccessor httpContextAccessor, + IOptions webRoutingSettings) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _userManager = backOfficeUserManager; @@ -110,6 +116,50 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _linkGenerator = linkGenerator; _externalAuthenticationOptions = externalAuthenticationOptions; _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; + _httpContextAccessor = httpContextAccessor; + _webRoutingSettings = webRoutingSettings.Value; + } + + [Obsolete("Use constructor that also takes IHttpAccessor and IOptions, scheduled for removal in V11")] + public AuthenticationController( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IBackOfficeUserManager backOfficeUserManager, + IBackOfficeSignInManager signInManager, + IUserService userService, + ILocalizedTextService textService, + IUmbracoMapper umbracoMapper, + IOptions globalSettings, + IOptions securitySettings, + ILogger logger, + IIpResolver ipResolver, + IOptions passwordConfiguration, + IEmailSender emailSender, + ISmsSender smsSender, + IHostingEnvironment hostingEnvironment, + LinkGenerator linkGenerator, + IBackOfficeExternalLoginProviders externalAuthenticationOptions, + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) + : this( + backofficeSecurityAccessor, + backOfficeUserManager, + signInManager, + userService, + textService, + umbracoMapper, + globalSettings, + securitySettings, + logger, + ipResolver, + passwordConfiguration, + emailSender, + smsSender, + hostingEnvironment, + linkGenerator, + externalAuthenticationOptions, + backOfficeTwoFactorOptions, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>()) + { } /// @@ -629,7 +679,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }); // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _hostingEnvironment.ApplicationMainUrl; + Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request.GetApplicationUri(_webRoutingSettings); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 79e7838110..72377c0670 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MimeKit; @@ -42,6 +43,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -75,7 +77,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; private readonly IPasswordChanger _passwordChanger; private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebRoutingSettings _webRoutingSettings; + [ActivatorUtilitiesConstructor] public UsersController( MediaFileManager mediaFileManager, IOptions contentSettings, @@ -96,7 +101,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalLogins, UserEditorAuthorizationHelper userEditorAuthorizationHelper, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IHttpContextAccessor httpContextAccessor, + IOptions webRoutingSettings) { _mediaFileManager = mediaFileManager; _contentSettings = contentSettings.Value; @@ -119,6 +126,55 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _userEditorAuthorizationHelper = userEditorAuthorizationHelper; _passwordChanger = passwordChanger; _logger = _loggerFactory.CreateLogger(); + _httpContextAccessor = httpContextAccessor; + _webRoutingSettings = webRoutingSettings.Value; + } + + [Obsolete("Use constructor that also takes IHttpAccessor and IOptions, scheduled for removal in V11")] + public UsersController( + MediaFileManager mediaFileManager, + IOptions contentSettings, + IHostingEnvironment hostingEnvironment, + ISqlContext sqlContext, + IImageUrlGenerator imageUrlGenerator, + IOptions securitySettings, + IEmailSender emailSender, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + IUserService userService, + ILocalizedTextService localizedTextService, + IUmbracoMapper umbracoMapper, + IOptions globalSettings, + IBackOfficeUserManager backOfficeUserManager, + ILoggerFactory loggerFactory, + LinkGenerator linkGenerator, + IBackOfficeExternalLoginProviders externalLogins, + UserEditorAuthorizationHelper userEditorAuthorizationHelper, + IPasswordChanger passwordChanger) + : this(mediaFileManager, + contentSettings, + hostingEnvironment, + sqlContext, + imageUrlGenerator, + securitySettings, + emailSender, + backofficeSecurityAccessor, + appCaches, + shortStringHelper, + userService, + localizedTextService, + umbracoMapper, + globalSettings, + backOfficeUserManager, + loggerFactory, + linkGenerator, + externalLogins, + userEditorAuthorizationHelper, + passwordChanger, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>()) + { } /// @@ -421,20 +477,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public async Task> PostInviteUser(UserInvite userSave) { - if (userSave == null) throw new ArgumentNullException("userSave"); + if (userSave == null) + { + throw new ArgumentNullException("userSave"); + } if (userSave.Message.IsNullOrWhiteSpace()) + { ModelState.AddModelError("Message", "Message cannot be empty"); + } IUser user; if (_securitySettings.UsernameIsEmail) { - //ensure it's the same + // ensure it's the same userSave.Username = userSave.Email; } else { - //first validate the username if we're showing it + // first validate the username if we're showing it var userResult = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); if (!(userResult.Result is null)) { @@ -443,6 +504,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers user = userResult.Value; } + user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); if (ModelState.IsValid == false) @@ -455,7 +517,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem("No Email server is configured"); } - //Perform authorization here to see if the current user can actually save this user with the info being requested + // Perform authorization here to see if the current user can actually save this user with the info being requested var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -464,8 +526,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (user == null) { - //we want to create the user with the UserManager, this ensures the 'empty' (special) password - //format is applied without us having to duplicate that logic + // we want to create the user with the UserManager, this ensures the 'empty' (special) password + // format is applied without us having to duplicate that logic var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; @@ -475,21 +537,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(created.Errors.ToErrorMessage()); } - //now re-look the user back up + // now re-look the user back up user = _userService.GetByEmail(userSave.Email); } - //map the save info over onto the user + // map the save info over onto the user user = _umbracoMapper.Map(userSave, user); - //ensure the invited date is set + // ensure the invited date is set user.InvitedDate = DateTime.Now; - //Save the updated user (which will process the user groups too) + // Save the updated user (which will process the user groups too) _userService.Save(user); var display = _umbracoMapper.Map(user); - //send the email + // send the email await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles","resendInviteHeader"), _localizedTextService.Localize("speechBubbles","resendInviteSuccess", new[] { user.Name })); @@ -544,14 +606,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }); // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _hostingEnvironment.ApplicationMainUrl; + Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request.GetApplicationUri(_webRoutingSettings); var inviteUri = new Uri(applicationUri, action); var emailSubject = _localizedTextService.Localize("user","inviteEmailCopySubject", - //Ensure the culture of the found user is used for the email! + // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings)); var emailBody = _localizedTextService.Localize("user","inviteEmailCopyFormat", - //Ensure the culture of the found user is used for the email! + // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), senderEmail }); diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs index c7c2bb3115..2aeb2555eb 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -1,9 +1,12 @@ -using System.IO; +using System; +using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; namespace Umbraco.Extensions @@ -107,5 +110,31 @@ namespace Umbraco.Extensions return result; } } + + /// + /// Gets the application URI, will use the one specified in settings if present + /// + public static Uri GetApplicationUri(this HttpRequest request, WebRoutingSettings routingSettings) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (routingSettings == null) + { + throw new ArgumentNullException(nameof(routingSettings)); + } + + if (string.IsNullOrEmpty(routingSettings.UmbracoApplicationUrl)) + { + var requestUri = new Uri(request.GetDisplayUrl()); + + // Create a new URI with the relative uri as /, this ensures that only the base path is returned. + return new Uri(requestUri, "/"); + } + + return new Uri(routingSettings.UmbracoApplicationUrl); + } } } From be53ccf1fd088d638cdd0ce2be2100f9402d7365 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Dec 2021 08:57:28 +0100 Subject: [PATCH 07/23] Implement #11776 for v9 --- .../Security/UmbracoApplicationUrlCheck.cs | 68 +++++++++++++++++++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 + .../umbraco/config/lang/en_us.xml | 2 + 3 files changed, 72 insertions(+) create mode 100644 src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs new file mode 100644 index 0000000000..44b10ba0e3 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -0,0 +1,68 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +{ + [HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] + public class UmbracoApplicationUrlCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IOptionsMonitor _webRoutingSettings; + + public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) + { + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } + + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var success = false; + + if (url.IsNullOrWhiteSpace()) + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + success = true; + } + + return new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck + }; + } + } +} diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 468a0cc735..4411209cd5 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -2290,6 +2290,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index aef05ca973..5a17eafbb2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -2372,6 +2372,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> From 0ea5f6a02c2497c69446bf3e65e4e54b24d7093e Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Dec 2021 08:58:13 +0100 Subject: [PATCH 08/23] Adding UmbracoApplicationUrlCheck "Read more" URL --- src/Umbraco.Core/Constants-HealthChecks.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 5770bd07e4..5a8ea401cb 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { /// /// Defines constants. @@ -20,15 +20,16 @@ public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; } + public static class Configuration { public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; } + public static class FolderAndFilePermissionsCheck { - public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; @@ -37,7 +38,7 @@ public static class Security { - + public const string UmbracoApplicationUrlCheck = "https://umbra.co/healthchecks-umbraco-application-url"; public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; @@ -46,7 +47,6 @@ public static class HttpsCheck { - public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; From 76cf5037e53b526977ff94083ac341a8e7dd9909 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 21 Dec 2021 14:02:49 +0100 Subject: [PATCH 09/23] v9: Move local xml package files to database instead (#11654) * Move package to database * Added migration and implemented new repository * Updated migrations to use proper xml convert * Fixed save function and renamed createDate to update date * Updated dependencyInjection * Updated UmbracoPlan.cs * Apply suggestions from code review Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Added File check * Tried using same context as create table * Fix DTO * Fix GetById and local package saving * Fix images when migrating * Implement deletion of all local files * Only delete local repo file, not file snapshots * Remove static package path and use the one we save * Update package repo to export package and remove check for ids when exporting * Minor fixes * Fix so that you can download package after creating * Update savePackage method to export package afterwards Co-authored-by: Nikolaj Geisle Co-authored-by: Elitsa Marinovska Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> --- .../Extensions/ContentExtensions.cs | 2 +- .../Packaging/PackagesRepository.cs | 9 + .../Persistence/Constants-DatabaseSchema.cs | 2 + .../UmbracoBuilder.Services.cs | 8 +- .../Install/DatabaseSchemaCreator.cs | 3 +- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_9_2_0/MovePackageXMLToDb.cs | 72 ++ .../Dtos/CreatedPackageSchemaDto.cs | 38 + .../CreatedPackageSchemaRepository.cs | 758 ++++++++++++++++++ .../Controllers/PackageController.cs | 10 +- .../Packaging/CreatedPackageSchemaTests.cs | 62 ++ 11 files changed, 953 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/MovePackageXMLToDb.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index daca62926a..67c483a4d6 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -367,7 +367,7 @@ namespace Umbraco.Extensions /// to generate xml for /// /// Xml representation of the passed in - internal static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) + public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) { return serializer.Serialize(content, false, true); } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 2ab24fa593..331034e787 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Packaging /// /// Manages the storage of installed/created package definitions /// + [Obsolete("Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] public class PackagesRepository : ICreatedPackagesRepository { private readonly IContentService _contentService; @@ -744,5 +745,13 @@ namespace Umbraco.Cms.Core.Packaging var packagesXml = XDocument.Load(packagesFile); return packagesXml; } + + public void DeleteLocalRepositoryFiles() + { + var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + File.Delete(packagesFile); + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + Directory.Delete(packagesFolder); + } } } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 680eee5ba2..37560b4c0a 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -81,6 +81,8 @@ namespace Umbraco.Cms.Core public const string UserLogin = TableNamePrefix + "UserLogin"; public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; + + public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; } } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 661ed93292..debe476c49 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -15,6 +15,8 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; @@ -74,16 +76,14 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); return builder; } - /// - /// Creates an instance of PackagesRepository for either the ICreatedPackagesRepository or the IInstalledPackagesRepository - /// private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) => new PackagesRepository( factory.GetRequiredService(), diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index d9ebb8bf75..8fb9767eb7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -78,7 +78,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install typeof(ContentScheduleDto), typeof(LogViewerQueryDto), typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto) + typeof(UserGroup2NodeDto), + typeof(CreatedPackageSchemaDto) }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index ff9e86335f..2b6f2fe6d6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -268,6 +268,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // TO 9.2.0 To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); + To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/MovePackageXMLToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/MovePackageXMLToDb.cs new file mode 100644 index 0000000000..e4a4af0cbb --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/MovePackageXMLToDb.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +{ + public class MovePackageXMLToDb : MigrationBase + { + private readonly PackagesRepository _packagesRepository; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) + : base(context) + { + _packagesRepository = packagesRepository; + _xmlParser = new PackageDefinitionXmlParser(); + } + + private void CreateDatabaseTable() + { + // Add CreatedPackage table in database if it doesn't exist + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) + { + Create.Table().Do(); + } + } + + private void MigrateCreatedPackageFilesToDb() + { + // Load data from file + IEnumerable packages = _packagesRepository.GetAll(); + var createdPackageDtos = new List(); + foreach (PackageDefinition package in packages) + { + // Create dto from xmlDocument + var dto = new CreatedPackageSchemaDto() + { + Name = package.Name, + Value = _xmlParser.ToXml(package).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() + }; + createdPackageDtos.Add(dto); + } + + _packagesRepository.DeleteLocalRepositoryFiles(); + if (createdPackageDtos.Any()) + { + // Insert dto into CreatedPackage table + Database.InsertBulk(createdPackageDtos); + } + } + + /// + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateCreatedPackageFilesToDb(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs new file mode 100644 index 0000000000..37e6fd8d8d --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -0,0 +1,38 @@ +using System; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +{ + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("id")] + public class CreatedPackageSchemaDto + { + public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } + + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } + + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [Column("packageId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid PackageId { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs new file mode 100644 index 0000000000..0c4f876bb1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -0,0 +1,758 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; +using File = System.IO.File; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + /// + public class CreatedPackageSchemaRepository : ICreatedPackagesRepository + { + private readonly PackageDefinitionXmlParser _xmlParser; + private readonly IUmbracoDatabase _umbracoDatabase; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly FileSystems _fileSystems; + private readonly IEntityXmlSerializer _serializer; + private readonly IDataTypeService _dataTypeService; + private readonly ILocalizationService _localizationService; + private readonly IFileService _fileService; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IContentService _contentService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMacroService _macroService; + private readonly IContentTypeService _contentTypeService; + private readonly string _tempFolderPath; + private readonly string _mediaFolderPath; + + /// + /// Initializes a new instance of the class. + /// + public CreatedPackageSchemaRepository( + IUmbracoDatabase umbracoDatabase, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string mediaFolderPath = null, + string tempFolderPath = null) + { + _umbracoDatabase = umbracoDatabase; + _hostingEnvironment = hostingEnvironment; + _fileSystems = fileSystems; + _serializer = serializer; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _fileService = fileService; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _contentService = contentService; + _mediaFileManager = mediaFileManager; + _macroService = macroService; + _contentTypeService = contentTypeService; + _xmlParser = new PackageDefinitionXmlParser(); + _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _tempFolderPath = + tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; + } + + public IEnumerable GetAll() + { + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Select() + .From() + .OrderBy(x => x.Id); + + var packageDefinitions = new List(); + + List xmlSchemas = _umbracoDatabase.Fetch(query); + foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) + { + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + packageDefinitions.Add(packageDefinition); + } + + return packageDefinitions; + } + + public PackageDefinition GetById(int id) + { + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Select() + .From() + .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); + + if (schemaDtos.IsCollectionEmpty()) + { + return null; + } + + var packageSchema = schemaDtos.First(); + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + return packageDefinition; + } + + public void Delete(int id) + { + // Delete package snapshot + var packageDef = GetById(id); + if (File.Exists(packageDef.PackagePath)) + { + File.Delete(packageDef.PackagePath); + } + + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Delete() + .Where(x => x.Id == id); + + _umbracoDatabase.Delete(query); + } + + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) + { + throw new NullReferenceException("PackageDefinition cannot be null when saving"); + } + + if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + { + return false; + } + + // Ensure it's valid + ValidatePackage(definition); + + + if (definition.Id == default) + { + // Create dto from definition + var dto = new CreatedPackageSchemaDto() + { + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() + }; + + // Set the ids, we have to save in database first to get the Id + definition.PackageId = dto.PackageId; + var result = _umbracoDatabase.Insert(dto); + var decimalResult = result.SafeCast(); + definition.Id = decimal.ToInt32(decimalResult); + } + + // Save snapshot locally, we do this to the updated packagePath + ExportPackage(definition); + // Create dto from definition + var updatedDto = new CreatedPackageSchemaDto() + { + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + Id = definition.Id, + PackageId = definition.PackageId, + UpdateDate = DateTime.Now + }; + _umbracoDatabase.Update(updatedDto); + + return true; + } + + public string ExportPackage(PackageDefinition definition) + { + + // Ensure it's valid + ValidatePackage(definition); + + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); + if (Directory.Exists(temporaryPath) == false) + { + Directory.CreateDirectory(temporaryPath); + } + + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", + _fileSystems.PartialViewsFileSystem); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) + { + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) + { + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } + } + } + } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = + _hostingEnvironment.MapPathWebRoot( + Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); + + if (Directory.Exists(directoryName) == false) + { + Directory.CreateDirectory(directoryName); + } + + var finalPackagePath = Path.Combine(directoryName, fileName); + + if (File.Exists(finalPackagePath)) + { + File.Delete(finalPackagePath); + } + + if (File.Exists(finalPackagePath.Replace("zip", "xml"))) + { + File.Delete(finalPackagePath.Replace("zip", "xml")); + } + + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + private XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private void ValidatePackage(PackageDefinition definition) + { + // Ensure it's valid + var context = new ValidationContext(definition, serviceProvider: null, items: null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDataType dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) + { + continue; + } + + dataTypes.Add(_serializer.Serialize(dataType)); + } + + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage lang = _localizationService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem di = _localizationService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) + { + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + } + else + { + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) + { + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, + serializedDictionaryValue); + } + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) + { + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop + continue; + } + else + { + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, + serializedDictionaryValue); + } + } + } + } + + root.Add(rootDictionaryItems); + + static void AppendDictionaryElement(XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, Guid key, XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) + { + continue; + } + + XElement macroXml = GetMacroXml(outInt, out IMacro macro); + if (macroXml == null) + { + continue; + } + + macros.Add(macroXml); + packagedMacros.Add(macro); + } + + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros + .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => + x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) + { + if (stylesheet.IsNullOrWhiteSpace()) + { + continue; + } + + XElement xml = GetStylesheetXml(stylesheet, true); + if (xml != null) + { + stylesheetsXml.Add(xml); + } + } + + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) + { + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem.FileExists(file)) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem.OpenFile(file)) + using (var reader = new StreamReader(stream)) + { + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); + } + } + + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) + { + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ITemplate template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } + + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, + NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent content = _contentService.GetById(contentNodeId); + if (content != null) + { + var contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + + root.Add( + new XElement( + "Documents", + new XElement( + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); + } + } + } + } + + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); + + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } + } + + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + /// + /// Gets a macros xml node + /// + private XElement GetMacroXml(int macroId, out IMacro macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; + } + + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + private XElement GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + IStylesheet stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType parent = _contentTypeService.Get(dt.ParentId); + if (parent != null) + { + AddDocumentType(parent, dtl); + } + } + + if (!dtl.Contains(dt)) + { + dtl.Add(dt); + } + } + + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType parent = _mediaTypeService.Get(mediaType.ParentId); + if (parent != null) + { + AddMediaType(parent, mediaTypes); + } + } + + if (!mediaTypes.Contains(mediaType)) + { + mediaTypes.Add(mediaType); + } + } + + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); + + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 813682c70e..6312b935f9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) return ValidationProblem(ModelState); - //save it + // Save it if (!_packagingService.SaveCreatedPackage(model)) { return ValidationProblem( @@ -78,9 +78,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers : $"The package with id {model.Id} was not found"); } - _packagingService.ExportCreatedPackage(model); - - //the packagePath will be on the model + // The packagePath will be on the model return model; } @@ -112,11 +110,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationErrorResult.CreateNotificationValidationErrorResult( $"Package migration failed on package {packageName} with error: {ex.Message}. Check log for full details."); - } + } } [HttpGet] - public IActionResult DownloadCreatedPackage(int id) + public IActionResult DownloadCreatedPackage(int id) { var package = _packagingService.GetCreatedPackageById(id); if (package == null) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs new file mode 100644 index 0000000000..6a5ee88426 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class CreatedPackageSchemaTests : UmbracoIntegrationTest + { + private ICreatedPackagesRepository CreatedPackageSchemaRepository => + GetRequiredService(); + + [Test] + public void PackagesRepository_Can_Save_PackageDefinition() + { + var packageDefinition = new PackageDefinition() + { + Name = "NewPack", DocumentTypes = new List() { "Root" } + }; + var result = CreatedPackageSchemaRepository.SavePackage(packageDefinition); + Assert.IsTrue(result); + } + + [Test] + public void PackageRepository_GetAll_Returns_All_PackageDefinitions() + { + var packageDefinitionList = new List() + { + new () { Name = "PackOne" }, + new () { Name = "PackTwo" }, + new () { Name = "PackThree" } + }; + foreach (PackageDefinition packageDefinition in packageDefinitionList) + { + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + } + + var loadedPackageDefinitions = CreatedPackageSchemaRepository.GetAll().ToList(); + CollectionAssert.IsNotEmpty(loadedPackageDefinitions); + CollectionAssert.AllItemsAreUnique(loadedPackageDefinitions); + Assert.AreEqual(loadedPackageDefinitions.Count, 3); + } + + [Test] + public void PackageRepository_Can_Update_Package() + { + var packageDefinition = new PackageDefinition() { Name = "TestPackage" }; + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + + packageDefinition.Name = "UpdatedName"; + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + var result = CreatedPackageSchemaRepository.GetAll().ToList(); + + Assert.AreEqual(result.Count, 1); + Assert.AreEqual(result.FirstOrDefault()?.Name, "UpdatedName"); + } + } +} From 1af09c7f1345484d6cf1c91172935ad6d781e99a Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Dec 2021 15:19:55 +0100 Subject: [PATCH 10/23] Updates Forms and adds Deploy JSON schema. (#11739) --- src/JsonSchema/AppSettings.cs | 8 +++++++- src/JsonSchema/JsonSchema.csproj | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 8db2bbb8c7..a9b70fc4a1 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -1,7 +1,9 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Deploy.Core.Configuration.DeployConfiguration; +using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration; using Umbraco.Forms.Core.Configuration; using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings; @@ -82,6 +84,7 @@ namespace JsonSchema public BasicAuthSettings BasicAuth { get; set; } public PackageMigrationSettings PackageMigration { get; set; } + public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } } @@ -116,6 +119,9 @@ namespace JsonSchema /// public class DeployDefinition { + public DeploySettings Settings { get; set; } + + public DeployProjectConfig Project { get; set; } } } } diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 84de67b657..c7cbbe1320 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -10,7 +10,8 @@ - + + From bc6d8b5ecee275b77942714a0075b50a2412ba8d Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:03:38 +0100 Subject: [PATCH 11/23] v9: Throw error on duplicate routes (#11774) * Add conflicting route service and check * Added testclass * Cleanup * Update src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Implemented out variable Co-authored-by: Elitsa Marinovska Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> --- .../Services/IConflictingRouteService.cs | 7 ++++ .../Runtime/RuntimeState.cs | 34 +++++++++++++++++- .../UmbracoBuilderExtensions.cs | 1 + .../Services/ConflictingRouteService.cs | 35 +++++++++++++++++++ .../Testing/TestConflictingRouteService.cs | 14 ++++++++ .../Testing/UmbracoIntegrationTest.cs | 3 ++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Core/Services/IConflictingRouteService.cs create mode 100644 src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs create mode 100644 tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs diff --git a/src/Umbraco.Core/Services/IConflictingRouteService.cs b/src/Umbraco.Core/Services/IConflictingRouteService.cs new file mode 100644 index 0000000000..04d81d7f88 --- /dev/null +++ b/src/Umbraco.Core/Services/IConflictingRouteService.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services +{ + public interface IConflictingRouteService + { + public bool HasConflictingRoutes(out string controllerName); + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 8eab08bfee..73b6692e3a 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,6 +13,7 @@ using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Runtime @@ -30,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly ILogger _logger; private readonly PendingPackageMigrations _packageMigrationState; private readonly Dictionary _startupState = new Dictionary(); + private readonly IConflictingRouteService _conflictingRouteService; /// /// The initial @@ -51,6 +54,25 @@ namespace Umbraco.Cms.Infrastructure.Runtime IUmbracoDatabaseFactory databaseFactory, ILogger logger, PendingPackageMigrations packageMigrationState) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService) { _globalSettings = globalSettings; _unattendedSettings = unattendedSettings; @@ -58,9 +80,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _logger = logger; _packageMigrationState = packageMigrationState; + _conflictingRouteService = conflictingRouteService; } - /// public Version Version => _umbracoVersion.Version; @@ -101,6 +123,16 @@ namespace Umbraco.Cms.Infrastructure.Runtime return; } + // Check if we have multiple controllers with the same name. + if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) + { + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException($"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"); + + return; + } + // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... switch (GetUmbracoDatabaseState(_databaseFactory)) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index aeb329efb4..7a15d640e6 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -114,6 +114,7 @@ namespace Umbraco.Extensions }); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); return builder; diff --git a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs new file mode 100644 index 0000000000..2951ace9e1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; + +namespace Umbraco.Cms.Web.BackOffice.Services +{ + public class ConflictingRouteService : IConflictingRouteService + { + private readonly TypeLoader _typeLoader; + + /// + /// Initializes a new instance of the class. + /// + public ConflictingRouteService(TypeLoader typeLoader) => _typeLoader = typeLoader; + + /// + public bool HasConflictingRoutes(out string controllerName) + { + var controllers = _typeLoader.GetTypes().ToList(); + foreach (Type controller in controllers) + { + if (controllers.Count(x => x.Name == controller.Name) > 1) + { + controllerName = controller.Name; + return true; + } + } + + controllerName = string.Empty; + return false; + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs b/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs new file mode 100644 index 0000000000..c61cbb8a1d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs @@ -0,0 +1,14 @@ +using System; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.Testing +{ + public class TestConflictingRouteService : IConflictingRouteService + { + public bool HasConflictingRoutes(out string controllername) + { + controllername = string.Empty; + return false; + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 314a614b49..fb505e6b83 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -201,6 +201,9 @@ namespace Umbraco.Cms.Tests.Integration.Testing IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); + // We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests + services.AddSingleton(); + // Add it! Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); TypeLoader typeLoader = services.AddTypeLoader( From 84fea8f9537c37c8ed5ee7601465e4ee5ea1df0f Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 3 Jan 2022 15:07:44 +0100 Subject: [PATCH 12/23] Fix assignDomain to handle case sensitive operating systems (#11784) --- src/Umbraco.Core/Actions/ActionAssignDomain.cs | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/cs.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/cy.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/de.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/es.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/he.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/it.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/ja.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/ko.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/nb.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/nl.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/pl.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/pt.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/ru.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/tr.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/zh.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index e03e2de81c..6340a03082 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -8,7 +8,7 @@ public const char ActionLetter = 'I'; public char Letter => ActionLetter; - public string Alias => "assignDomain"; + public string Alias => "assigndomain"; public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; public string Icon => "home"; public bool ShowInNotifier => false; diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml index af701cd5e3..a90aa33355 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml @@ -60,7 +60,7 @@ Ostatní - Povolit přístup k přiřazování kultury a názvů hostitelů + Povolit přístup k přiřazování kultury a názvů hostitelů Povolit přístup k zobrazení protokolu historie uzlu Povolit přístup k zobrazení uzlu Povolit přístup ke změně typu dokumentu daného uzlu diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml index 0692d01e7a..60ff3ffdb1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml @@ -5,7 +5,7 @@ https://www.method4.co.uk/ - Diwylliannau ac Enwau Gwesteia + Diwylliannau ac Enwau Gwesteia Trywydd Archwilio Dewis Nod Newid Math o Ddogfen diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 27901846a9..1f6dcff88f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Tilføj domæne + Tilføj domæne Revisionsspor Gennemse elementer Skift Dokument Type diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index 8f2ba350d0..0cabf29497 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Kulturen und Hostnamen + Kulturen und Hostnamen Protokoll Durchsuchen Dokumenttyp ändern diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 4411209cd5..47c104b822 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture and Hostnames + Culture and Hostnames Audit Trail Browse Node Change Document Type diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 5a17eafbb2..e4d2784c8c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture and Hostnames + Culture and Hostnames Audit Trail Browse Node Change Document Type diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index df78683aca..d99548f5c5 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Administrar dominios + Administrar dominios Historial Nodo de Exploración Cambiar tipo de documento diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 4681818c47..68dc28c99b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture et noms d'hôte + Culture et noms d'hôte Informations d'audit Parcourir Changer le type de document diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml index 9ee8bbf014..b99890282e 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - נהל שמות מתחם + נהל שמות מתחם מעקב ביקורות צפה בתוכן העתק diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml index a0d89bff2d..7e20c0b266 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Gestisci hostnames + Gestisci hostnames Audit Trail Sfoglia Cambia tipo di documento diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml index 4b98adad26..142f7d055b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - ドメインの割り当て + ドメインの割り当て 動作記録 ノードの参照 ドキュメントタイプの変更 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml index 792dd6700c..c340f3f30a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 호스트명 관리 + 호스트명 관리 감사 추적 노드 탐색 복사 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml index 1c47969189..44fad5d2ec 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Angi domene + Angi domene Revisjoner Bla gjennom Skift dokumenttype diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index f830c3368d..0c24b119c9 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Beheer domeinnamen + Beheer domeinnamen Documentgeschiedenis Node bekijken Documenttype wijzigen diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml index dfbc324df6..712b695f77 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Zarządzanie hostami + Zarządzanie hostami Historia zmian Przeglądaj węzeł Zmień typ dokumentu diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml index 542b03abc1..eac232e851 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Gerenciar hostnames + Gerenciar hostnames Caminho de Auditoria Navegar o Nó Copiar diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 9c1d9e12fb..5c18fbc682 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Языки и домены + Языки и домены История исправлений Просмотреть Изменить тип документа diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index e0e2235ae9..dda58366d8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -8,7 +8,7 @@ Innehåll - Hantera domännamn + Hantera domännamn Hantera versioner Surfa på sidan Ändra dokumenttyp diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml index 58c0f7f94b..66d097b1e9 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Kültür ve Ana Bilgisayar Adları + Kültür ve Ana Bilgisayar Adları Denetim Yolu Düğüme Göz At Belge Türünü Değiştir diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml index ba51488f9f..8c78154b62 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 管理主机名 + 管理主机名 跟踪审计 浏览节点 改变文档类型 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml index 8d5cf16de2..992a7ba55b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 管理主機名稱 + 管理主機名稱 跟蹤審計 流覽節點 改變文檔類型 From 642c216f944b507a234ee8c09a9c8b5cdd180dbe Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 6 Jan 2022 13:35:24 +0100 Subject: [PATCH 13/23] Serve Media and App_Plugins using WebRootFileProvider (and allow changing the physical media path) (#11783) * Allow changing UmbracoMediaPath to an absolute path. Also ensure Imagesharp are handing requests outside of the wwwroot folder. * Let UmbracoMediaUrl fallback to UmbracoMediaPath when empty * Add FileSystemFileProvider to expose an IFileSystem as IFileProvider * Replace IUmbracoMediaFileProvider with IFileProviderFactory implementation * Fix issue resolving relative paths when media URL has changed * Remove FileSystemFileProvider and require explicitly implementing IFileProviderFactory * Update tests (UnauthorizedAccessException isn't thrown anymore for rooted files) * Update test to use UmbracoMediaUrl * Add UmbracoMediaPhysicalRootPath global setting * Remove MediaFileManagerImageProvider and use composited file providers * Move CreateFileProvider to IFileSystem extension method * Add rooted path tests Co-authored-by: Ronald Barendse --- .../Configuration/Models/GlobalSettings.cs | 31 ++++++++++------ .../Constants-SystemDirectories.cs | 2 ++ .../UmbracoBuilder.Configuration.cs | 26 +++++++++----- src/Umbraco.Core/IO/FileSystemExtensions.cs | 20 +++++++++++ src/Umbraco.Core/IO/IFileProviderFactory.cs | 18 ++++++++++ src/Umbraco.Core/IO/MediaFileManager.cs | 35 +++++++++++-------- src/Umbraco.Core/IO/PhysicalFileSystem.cs | 18 ++++++---- src/Umbraco.Core/IO/ShadowWrapper.cs | 8 +++-- .../Packaging/PackagesRepository.cs | 2 +- .../Runtime/EssentialDirectoryCreator.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 3 +- .../UmbracoBuilder.FileSystems.cs | 2 +- .../Install/FilePermissionHelper.cs | 4 +-- .../CreatedPackageSchemaRepository.cs | 2 +- .../UmbracoApplicationBuilder.cs | 30 +++++++++++++--- .../ImageSharpConfigurationOptions.cs | 4 +-- .../UmbracoBuilder.ImageSharp.cs | 1 + .../ApplicationBuilderExtensions.cs | 31 ++++++++-------- .../Extensions/FileProviderExtensions.cs | 19 ++++++++++ .../PartialViewRepositoryTests.cs | 29 +++++++++------ .../Repositories/ScriptRepositoryTest.cs | 15 +++++--- .../Repositories/StylesheetRepositoryTest.cs | 17 ++++++--- .../Scoping/ScopeFileSystemsTests.cs | 6 ++-- .../TestHelpers/TestHelper.cs | 4 +-- 24 files changed, 233 insertions(+), 96 deletions(-) create mode 100644 src/Umbraco.Core/IO/IFileProviderFactory.cs create mode 100644 src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 97fb91b0ec..7e3e1a2700 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -31,21 +31,19 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticSanitizeTinyMce = false; /// - /// Gets or sets a value for the reserved URLs. - /// It must end with a comma + /// Gets or sets a value for the reserved URLs (must end with a comma). /// [DefaultValue(StaticReservedUrls)] public string ReservedUrls { get; set; } = StaticReservedUrls; /// - /// Gets or sets a value for the reserved paths. - /// It must end with a comma + /// Gets or sets a value for the reserved paths (must end with a comma). /// [DefaultValue(StaticReservedPaths)] public string ReservedPaths { get; set; } = StaticReservedPaths; /// - /// Gets or sets a value for the timeout + /// Gets or sets a value for the back-office login timeout. /// [DefaultValue(StaticTimeOut)] public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); @@ -104,11 +102,19 @@ namespace Umbraco.Cms.Core.Configuration.Models public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; /// - /// Gets or sets a value for the Umbraco media path. + /// Gets or sets a value for the Umbraco media request path. /// [DefaultValue(StaticUmbracoMediaPath)] public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; + /// + /// Gets or sets a value for the physical Umbraco media root path (falls back to when empty). + /// + /// + /// If the value is a virtual path, it's resolved relative to the webroot. + /// + public string UmbracoMediaPhysicalRootPath { get; set; } + /// /// Gets or sets a value indicating whether to install the database when it is missing. /// @@ -131,6 +137,9 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets the telemetry ID. + /// public string Id { get; set; } = string.Empty; /// @@ -164,19 +173,19 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); - /// Gets a value indicating whether TinyMCE scripting sanitization should be applied + /// + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied. /// [DefaultValue(StaticSanitizeTinyMce)] public bool SanitizeTinyMce => StaticSanitizeTinyMce; /// - /// An int value representing the time in milliseconds to lock the database for a write operation + /// Gets a value representing the time in milliseconds to lock the database for a write operation. /// /// - /// The default value is 5000 milliseconds + /// The default value is 5000 milliseconds. /// - /// The timeout in milliseconds. [DefaultValue(StaticSqlWriteLockTimeOut)] public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index 80b49781ec..bf34aab989 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -43,6 +43,8 @@ namespace Umbraco.Cms.Core public const string AppPlugins = "/App_Plugins"; public static string AppPluginIcons => "/Backoffice/Icons"; + public const string CreatedPackages = "/created-packages"; + public const string MvcViews = "~/Views"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 6ef87464e8..ce2e4f2304 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -13,23 +13,25 @@ namespace Umbraco.Cms.Core.DependencyInjection public static partial class UmbracoBuilderExtensions { - private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder) + private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action> configure = null) where TOptions : class { var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); - if (umbracoOptionsAttribute is null) { - throw new ArgumentException("typeof(TOptions) do not have the UmbracoOptionsAttribute"); + throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); } - - builder.Services.AddOptions() - .Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), - o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) + var optionsBuilder = builder.Services.AddOptions() + .Bind( + builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), + o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties + ) .ValidateDataAnnotations(); - return builder; + configure?.Invoke(optionsBuilder); + + return builder; } /// @@ -52,7 +54,13 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions() + .AddUmbracoOptions(optionsBuilder => optionsBuilder.PostConfigure(options => + { + if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath)) + { + options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath; + } + })) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 23be195e4b..c95d37e1c3 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -3,6 +3,7 @@ using System.IO; using System.Security.Cryptography; using System.Text; using System.Threading; +using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core.IO; namespace Umbraco.Extensions @@ -87,5 +88,24 @@ namespace Umbraco.Extensions } fs.DeleteFile(tempFile); } + + /// + /// Creates a new from the file system. + /// + /// The file system. + /// When this method returns, contains an created from the file system. + /// + /// true if the was successfully created; otherwise, false. + /// + public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider) + { + fileProvider = fileSystem switch + { + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null + }; + + return fileProvider != null; + } } } diff --git a/src/Umbraco.Core/IO/IFileProviderFactory.cs b/src/Umbraco.Core/IO/IFileProviderFactory.cs new file mode 100644 index 0000000000..742467ccc8 --- /dev/null +++ b/src/Umbraco.Core/IO/IFileProviderFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Cms.Core.IO +{ + /// + /// Factory for creating instances. + /// + public interface IFileProviderFactory + { + /// + /// Creates a new instance. + /// + /// + /// The newly created instance (or null if not supported). + /// + IFileProvider Create(); + } +} diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index 96680d3f84..c769b9801e 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -22,13 +21,22 @@ namespace Umbraco.Cms.Core.IO private readonly IShortStringHelper _shortStringHelper; private readonly IServiceProvider _serviceProvider; private MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly ContentSettings _contentSettings; - /// - /// Gets the media filesystem. - /// - public IFileSystem FileSystem { get; } + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider) + { + _mediaPathScheme = mediaPathScheme; + _logger = logger; + _shortStringHelper = shortStringHelper; + _serviceProvider = serviceProvider; + FileSystem = fileSystem; + } + [Obsolete("Use the ctr that doesn't include unused parameters.")] public MediaFileManager( IFileSystem fileSystem, IMediaPathScheme mediaPathScheme, @@ -36,14 +44,13 @@ namespace Umbraco.Cms.Core.IO IShortStringHelper shortStringHelper, IServiceProvider serviceProvider, IOptions contentSettings) - { - _mediaPathScheme = mediaPathScheme; - _logger = logger; - _shortStringHelper = shortStringHelper; - _serviceProvider = serviceProvider; - _contentSettings = contentSettings.Value; - FileSystem = fileSystem; - } + : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) + { } + + /// + /// Gets the media filesystem. + /// + public IFileSystem FileSystem { get; } /// /// Delete media files. diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index db10cae416..3da09a499c 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -3,14 +3,17 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - public interface IPhysicalFileSystem : IFileSystem {} - public class PhysicalFileSystem : IPhysicalFileSystem + public interface IPhysicalFileSystem : IFileSystem + { } + + public class PhysicalFileSystem : IPhysicalFileSystem, IFileProviderFactory { private readonly IIOHelper _ioHelper; private readonly ILogger _logger; @@ -28,7 +31,7 @@ namespace Umbraco.Cms.Core.IO // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; - public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) + public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) { _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -270,7 +273,7 @@ namespace Umbraco.Cms.Core.IO return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); // unchanged - what else? - return path; + return path.TrimStart(Constants.CharArrays.ForwardSlash); } /// @@ -285,7 +288,7 @@ namespace Umbraco.Cms.Core.IO public string GetFullPath(string path) { // normalize - var opath = path; + var originalPath = path; path = EnsureDirectorySeparatorChar(path); // FIXME: this part should go! @@ -318,7 +321,7 @@ namespace Umbraco.Cms.Core.IO // nothing prevents us to reach the file, security-wise, yet it is outside // this filesystem's root - throw - throw new UnauthorizedAccessException($"File original: [{opath}] full: [{path}] is outside this filesystem's root."); + throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root."); } /// @@ -450,6 +453,9 @@ namespace Umbraco.Cms.Core.IO } } + /// + public IFileProvider Create() => new PhysicalFileProvider(_rootPath); + #endregion } } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index cda61cf7b5..7bf315a575 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -1,14 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - internal class ShadowWrapper : IFileSystem + internal class ShadowWrapper : IFileSystem, IFileProviderFactory { private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; @@ -220,5 +221,8 @@ namespace Umbraco.Cms.Core.IO { FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); } + + /// + public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null; } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 331034e787..36b7a5d5d5 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -93,7 +93,7 @@ namespace Umbraco.Cms.Core.Packaging _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); _parser = new PackageDefinitionXmlParser(); _mediaService = mediaService; diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index a9564712c3..6c45e4d969 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.Runtime // ensure we have some essential directories // every other component can then initialize safely _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f36d7a8ee5..ee83d0677a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 6582cfb0c6..f66991fb68 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -49,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection ILogger logger = factory.GetRequiredService>(); GlobalSettings globalSettings = factory.GetRequiredService>().Value; - var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPath); + var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); }); diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 0ad2271d7e..1d228ebf98 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Infrastructure.Install hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) }; _packagesPermissionsDirs = new[] @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Infrastructure.Install EnsureFiles(_permissionFiles, out errors); report[FilePermissionTest.FileWriting] = errors.ToList(); - EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors); + EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors); report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); return report.Sum(x => x.Value.Count()) == 0; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 0c4f876bb1..be1a31c2c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -76,7 +76,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement _macroService = macroService; _contentTypeService = contentTypeService; _xmlParser = new PackageDefinitionXmlParser(); - _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index 9d30551071..58f66a6fb8 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,10 +1,16 @@ using System; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { @@ -22,12 +28,14 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder { AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); ApplicationServices = appBuilder.ApplicationServices; - RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); + RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); } public IServiceProvider ApplicationServices { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } /// @@ -78,18 +86,32 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder } /// - /// Registers the default required middleware to run Umbraco + /// Registers the default required middleware to run Umbraco. /// - /// public void RegisterDefaultRequiredMiddleware() { UseUmbracoCoreMiddleware(); AppBuilder.UseStatusCodePages(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. AppBuilder.UseImageSharp(); + + // Get media file provider and request path/URL + var mediaFileManager = AppBuilder.ApplicationServices.GetRequiredService(); + if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider mediaFileProvider)) + { + GlobalSettings globalSettings = AppBuilder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = AppBuilder.ApplicationServices.GetService(); + string mediaRequestPath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + + // Configure custom file provider for media + IWebHostEnvironment webHostEnvironment = AppBuilder.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); + } + AppBuilder.UseStaticFiles(); + AppBuilder.UseUmbracoPluginsStaticFiles(); // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one diff --git a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs index 628345dcd6..f8897e522c 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs @@ -7,7 +7,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection /// /// Configures the ImageSharp middleware options to use the registered configuration. /// - /// + /// public sealed class ImageSharpConfigurationOptions : IConfigureOptions { /// @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration; /// - /// Invoked to configure a instance. + /// Invoked to configure an instance. /// /// The options instance to configure. public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 4d621d348c..30331fd812 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -74,6 +74,7 @@ namespace Umbraco.Extensions .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) .AddProcessor(); + // Configure middleware to use the registered/shared ImageSharp configuration builder.Services.AddTransient, ImageSharpConfigurationOptions>(); return builder.Services; diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index cc035d116b..fc6a2904df 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,18 +1,20 @@ using System; using System.IO; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions { @@ -94,7 +96,8 @@ namespace Umbraco.Extensions throw new ArgumentNullException(nameof(app)); } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + return app; app.UseMiddleware(); @@ -109,25 +112,21 @@ namespace Umbraco.Extensions public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); - - // Ensure the plugin folder exists - Directory.CreateDirectory(pluginFolder); - - var fileProvider = new UmbracoPluginPhysicalFileProvider( - pluginFolder, - umbracoPluginSettings); - - app.UseStaticFiles(new StaticFileOptions + if (Directory.Exists(pluginFolder)) { - FileProvider = fileProvider, - RequestPath = Constants.SystemDirectories.AppPlugins - }); + var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); + + var pluginFileProvider = new UmbracoPluginPhysicalFileProvider( + pluginFolder, + umbracoPluginSettings); + + IWebHostEnvironment webHostEnvironment = app.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider)); + } return app; } } - } diff --git a/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs new file mode 100644 index 0000000000..35a2882bdd --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Extensions +{ + internal static class FileProviderExtensions + { + public static IFileProvider ConcatComposite(this IFileProvider fileProvider, params IFileProvider[] fileProviders) + { + var existingFileProviders = fileProvider switch + { + CompositeFileProvider compositeFileProvider => compositeFileProvider.FileProviders, + _ => new[] { fileProvider } + }; + + return new CompositeFileProvider(existingFileProviders.Concat(fileProviders)); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs index 02709f7f84..4c6204ee4e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs @@ -53,7 +53,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { var repository = new PartialViewRepository(fileSystems); - var partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; + IPartialView partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("test-path-1.cshtml")); Assert.AreEqual("test-path-1.cshtml", partialView.Path); @@ -62,10 +62,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos partialView = new PartialView(PartialViewType.PartialView, "path-2/test-path-2.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.cshtml")); - Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-2.cshtml"); + partialView = repository.Get("path-2/test-path-2.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); @@ -76,26 +76,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-3.cshtml"); + partialView = repository.Get("path-2/test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2\\test-path-3.cshtml"); + partialView = repository.Get("path-2\\test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = new PartialView(PartialViewType.PartialView, "\\test-path-4.cshtml") { Content = "// partialView" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + partialView = new PartialView(PartialViewType.PartialView, "..\\test-path-4.cshtml") { Content = "// partialView" }; + Assert.Throws(() => repository.Save(partialView)); - partialView = (PartialView)repository.Get("missing.cshtml"); + partialView = new PartialView(PartialViewType.PartialView, "\\test-path-5.cshtml") { Content = "// partialView" }; + repository.Save(partialView); + + partialView = repository.Get("\\test-path-5.cshtml"); + Assert.IsNotNull(partialView); + Assert.AreEqual("test-path-5.cshtml", partialView.Path); + Assert.AreEqual("/Views/Partials/test-path-5.cshtml", partialView.VirtualPath); + + partialView = repository.Get("missing.cshtml"); Assert.IsNull(partialView); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => partialView = (PartialView)repository.Get("\\test-path-4.cshtml")); - Assert.Throws(() => partialView = (PartialView)repository.Get("../../packages.config")); + Assert.Throws(() => partialView = repository.Get("..\\test-path-4.cshtml")); + Assert.Throws(() => partialView = repository.Get("../../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs index 28f9a9eff1..4721af14e1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs @@ -303,15 +303,22 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); - script = new Script("\\test-path-4.js") { Content = "// script" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + script = new Script("..\\test-path-4.js") { Content = "// script" }; + Assert.Throws(() => repository.Save(script)); + script = new Script("\\test-path-5.js") { Content = "// script" }; + repository.Save(script); + + script = repository.Get("\\test-path-5.js"); + Assert.IsNotNull(script); + Assert.AreEqual("test-path-5.js", script.Path); + Assert.AreEqual("/scripts/test-path-5.js", script.VirtualPath); + script = repository.Get("missing.js"); Assert.IsNull(script); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => script = repository.Get("\\test-path-4.js")); + Assert.Throws(() => script = repository.Get("..\\test-path-4.js")); Assert.Throws(() => script = repository.Get("../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs index d9bde0bca2..a32638ed4d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -275,7 +275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos repository.Save(stylesheet); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.css")); - Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-2.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2/test-path-2.css"); @@ -300,17 +300,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); - stylesheet = new Stylesheet("\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + stylesheet = new Stylesheet("..\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + Assert.Throws(() => repository.Save(stylesheet)); - // fixed in 7.3 - 7.2.8 used to throw + stylesheet = new Stylesheet("\\test-path-5.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + repository.Save(stylesheet); + + stylesheet = repository.Get("\\test-path-5.css"); + Assert.IsNotNull(stylesheet); + Assert.AreEqual("test-path-5.css", stylesheet.Path); + Assert.AreEqual("/css/test-path-5.css", stylesheet.VirtualPath); + stylesheet = repository.Get("missing.css"); Assert.IsNull(stylesheet); // #7713 changes behaviour to return null when outside the filesystem // to accomodate changing the CSS path and not flooding the backoffice with errors - stylesheet = repository.Get("\\test-path-4.css"); // outside the filesystem, does not exist + stylesheet = repository.Get("..\\test-path-4.css"); // outside the filesystem, does not exist Assert.IsNull(stylesheet); stylesheet = repository.Get("../packages.config"); // outside the filesystem, exists diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs index e30d7bbf55..7ea8e65eda 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs @@ -48,7 +48,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MediaFileManager_does_not_write_to_physical_file_system_when_scoped_if_scope_does_not_complete() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MediaFileManager_writes_to_physical_file_system_when_scoped_and_scope_is_completed() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -108,7 +108,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MultiThread() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 8dee7db12d..ba89c9775e 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -134,9 +134,9 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers /// public static string MapPathForTestFiles(string relativePath) => s_testHelperInternal.MapPathForTestFiles(relativePath); - public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath, Constants.SystemDirectories.AppPlugins }); + public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.AppPlugins }); - public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath }); + public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath }); public static void CreateDirectories(string[] directories) { From 187cac469e4e8193057a682c49d80e07246684bb Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 7 Jan 2022 08:55:40 +0100 Subject: [PATCH 14/23] Add package tests (#11824) --- .../cypress/integration/Packages/packages.ts | 161 ++++++++++++++++++ .../cypress/plugins/index.js | 10 +- .../cypress/support/commands.js | 1 + .../package-lock.json | 12 +- .../Umbraco.Tests.AcceptanceTest/package.json | 3 +- .../tsconfig.json | 3 +- 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts new file mode 100644 index 0000000000..80250502e6 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts @@ -0,0 +1,161 @@ +/// +import { + ContentBuilder, + DocumentTypeBuilder + } from 'umbraco-cypress-testhelpers'; + +context('Packages', () => { + const packageName = "TestPackage"; + const rootDocTypeName = "Test document type"; + const nodeName = "1) Home"; + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + function CreatePackage(contentId){ + const newPackage = { + id: 0, + packageGuid: "00000000-0000-0000-0000-000000000000", + name: "TestPackage", + packagePath: "", + contentLoadChildNodes: false, + contentNodeId: contentId, + macros: [], + languages: [], + dictionaryItems: [], + templates: [], + partialViews: [], + documentTypes: [], + mediaTypes: [], + stylesheets: [], + scripts: [], + dataTypes: [], + mediaUdis: [], + mediaLoadChildNodes: false + } + const url = "https://localhost:44331/umbraco/backoffice/umbracoapi/package/PostSavePackage"; + cy.umbracoApiRequest(url, 'POST', newPackage); + } + + function CreateSimplePackage(){ + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + cy.saveContent(rootContentNode).then((generatedContent) => { + CreatePackage(generatedContent.Id); + }); + }); + } + + it('Creates a simple package', () => { + + cy.umbracoEnsurePackageNameNotExists(packageName); + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + cy.saveContent(rootContentNode); + }); + + // Navigate to create package section + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('Create package').click(); + + // Fill out package creation form + cy.get('#headerName').should('be.visible'); + cy.wait(1000); + cy.get('#headerName').type(packageName); + cy.get('.controls > .umb-node-preview-add').click(); + cy.get('.umb-tree-item__label').first().click(); + cy.contains('Create').click(); + + // Navigate pack to packages and Assert the file is created + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains(packageName).should('be.visible'); + + // Cleanup + cy.umbracoEnsurePackageNameNotExists(packageName); + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('Delete package', () => { + + // Ensure cleanup before test + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + + CreateSimplePackage(); + + // Navigate to create package section + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('Delete').click(); + cy.contains('Yes, delete').click(); + + // Assert + cy.contains('TestPackage').should('not.exist'); + + // Cleanup + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + }); + + it('Download package', () => { + + // Ensure cleanup before test + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + + CreateSimplePackage(); + + // Navigate to package and download + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('TestPackage').click(); + cy.contains('Download').click(); + + // Assert + cy.verifyDownload('package.xml'); + + // Cleanup + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + }); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js b/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js index 51b79a1fef..30988b82cf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js @@ -12,6 +12,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) const del = require('del') +const { isFileExist } = require('cy-verify-downloads'); /** * @type {Cypress.PluginConfig} @@ -25,6 +26,7 @@ module.exports = (on, config) => { config.baseUrl = baseUrl; } + on('task', { isFileExist }) on('after:spec', (spec, results) => { if(results.stats.failures === 0 && results.video) { // `del()` returns a promise, so it's important to return it to ensure @@ -33,5 +35,11 @@ module.exports = (on, config) => { } }) + +on('task', { + isFileExist +}); + + return config; -} +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js b/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js index 4a2a6d31a2..5056c05036 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js @@ -27,6 +27,7 @@ import {Command} from 'umbraco-cypress-testhelpers'; import {Chainable} from './chainable'; import { JsonHelper } from 'umbraco-cypress-testhelpers'; +require('cy-verify-downloads').addCustomCommand(); new Chainable(); new Command().registerCypressCommands(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index a4636d990a..7ada1d9fb7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -453,6 +453,12 @@ "which": "^2.0.1" } }, + "cy-verify-downloads": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/cy-verify-downloads/-/cy-verify-downloads-0.0.5.tgz", + "integrity": "sha512-aRK7VvKG5rmDJK4hjZ27KM2oOOz0cMO7z/j4zX8qCc4ffXZS1XRJkofUY0w5u6MCB/wUsNMs03VuvkeR2tNPoQ==", + "dev": true + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -1622,9 +1628,9 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-62", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-62.tgz", - "integrity": "sha512-fVjXBdotb2TZrhaWq/HtD1cy+7scHcldJL+HRHeyYtwvUp368lUiAMrx0y4TOZTwBOJ898yyl8yTEPASfAX2pQ==", + "version": "1.0.0-beta-63", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-63.tgz", + "integrity": "sha512-X+DHWktfB+WBb7YrxvpneVfS1PATx2zPYMdkeZTmtoQEeyGxXA9fW6P712/AUbyGAhRhH+46t4cAINdWJxItug==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index a44877e703..a95a71020f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -10,10 +10,11 @@ "devDependencies": { "cross-env": "^7.0.2", "cypress": "8.4.1", + "cy-verify-downloads": "0.0.5", "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-62" + "umbraco-cypress-testhelpers": "^1.0.0-beta-63" }, "dependencies": { "typescript": "^3.9.2" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json index 6cb05bfcc7..96178bfc54 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json +++ b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json @@ -14,7 +14,8 @@ "target": "es5", "types": [ - "cypress" + "cypress", + "cy-verify-downloads" ], "lib": [ "es5", From a952d17e7423a0b43344c5aa79478b0424fb0ffb Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 10 Jan 2022 15:31:46 +0000 Subject: [PATCH 15/23] Make using scoped services in notification handlers less painful. (#11733) * Add failing test to demo issue with handlers + scoped dependencies. * Make using scoped services in notification handlers less painful. * Update comments for accuracy. --- .../Events/EventAggregator.Notifications.cs | 61 ++++++++++++++-- .../Implementations/TestHelper.cs | 6 ++ .../UmbracoTestServerTestBase.cs | 11 ++- .../Events/EventAggregatorTests.cs | 71 +++++++++++++++++++ 4 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index 58ccb06ed0..44eac93641 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Events @@ -84,17 +86,56 @@ namespace Umbraco.Cms.Core.Events internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper where TNotification : INotification { + /// + /// + /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event handlers.
+ /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. + ///
+ /// + /// + /// Some things worth knowing about MediatR. + /// + /// All handlers are by default registered with transient lifetime, but can easily depend on services with state. + /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always possible to depend on scoped services in a handler. + /// + /// + /// + /// + /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the registration was changed to singleton, presumably + /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to use scoped services from a singleton. + ///
+ /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate any scoped services + /// they wish to make use of e.g. a unit of work (think entity framework DBContext). + ///
+ /// + /// + /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean an awful lot of service location to avoid breaking changes. + ///
+ /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling the transient handlers to take a dependency on a scoped service. + ///
+ /// + /// + /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has http context, + /// but decided against because it's inconsistent with what happens in background threads and will just cause confusion. + /// + ///
public override Task HandleAsync( INotification notification, CancellationToken cancellationToken, ServiceFactory serviceFactory, Func>, INotification, CancellationToken, Task> publish) { - IEnumerable> handlers = serviceFactory - .GetInstances>() + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() .Select(x => new Func( (theNotification, theToken) => - x.HandleAsync((TNotification)theNotification, theToken))); + x.HandleAsync((TNotification)theNotification, theToken))); return publish(handlers, notification, cancellationToken); } @@ -103,13 +144,23 @@ namespace Umbraco.Cms.Core.Events internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper where TNotification : INotification { + /// + /// See remarks on for explanation on + /// what's going on with the IServiceProvider stuff here. + /// public override void Handle( INotification notification, ServiceFactory serviceFactory, Action>, INotification> publish) { - IEnumerable> handlers = serviceFactory - .GetInstances>() + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() .Select(x => new Action( (theNotification) => x.Handle((TNotification)theNotification))); diff --git a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs index a9bed62993..f8afe1d6ae 100644 --- a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -60,6 +60,11 @@ namespace Umbraco.Cms.Tests.Integration.Implementations _ipResolver = new AspNetCoreIpResolver(_httpContextAccessor); string contentRoot = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); + + // The mock for IWebHostEnvironment has caused a good few issues. + // We can UseContentRoot, UseWebRoot etc on the host builder instead. + // possibly going down rabbit holes though as would need to cleanup all usages of + // GetHostingEnvironment & GetWebHostEnvironment. var hostEnvironment = new Mock(); // This must be the assembly name for the WebApplicationFactory to work. @@ -68,6 +73,7 @@ namespace Umbraco.Cms.Tests.Integration.Implementations hostEnvironment.Setup(x => x.ContentRootFileProvider).Returns(() => new PhysicalFileProvider(contentRoot)); hostEnvironment.Setup(x => x.WebRootPath).Returns(() => WorkingDirectory); hostEnvironment.Setup(x => x.WebRootFileProvider).Returns(() => new PhysicalFileProvider(WorkingDirectory)); + hostEnvironment.Setup(x => x.EnvironmentName).Returns("Tests"); // We also need to expose it as the obsolete interface since netcore's WebApplicationFactory casts it. hostEnvironment.As(); diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 2d140e4336..bd9418bdb6 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -87,7 +87,16 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest // call startup builder.Configure(app => Configure(app)); - }).UseEnvironment(Environments.Development); + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; + }); return builder; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs new file mode 100644 index 0000000000..6446bf542a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Tests.Integration.TestServerTest; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Events +{ + [TestFixture] + public class EventAggregatorTests : UmbracoTestServerTestBase + { + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddScoped(); + services.AddTransient, EventAggregatorTestNotificationHandler>(); + } + + [Test] + public async Task Publish_HandlerWithScopedDependency_DoesNotThrow() + { + HttpResponseMessage result = await Client.GetAsync("/test-handler-with-scoped-services"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + } + } + + public class EventAggregatorTestsController : Controller + { + private readonly IEventAggregator _eventAggregator; + + public EventAggregatorTestsController(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; + + [HttpGet("test-handler-with-scoped-services")] + public async Task Test() + { + var notification = new EventAggregatorTestNotification(); + await _eventAggregator.PublishAsync(notification); + + if (!notification.Mutated) + { + throw new ApplicationException("Doesn't work"); + } + + return Ok(); + } + } + + public class EventAggregatorTestScopedService + { + } + + public class EventAggregatorTestNotification : INotification + { + public bool Mutated { get; set; } + } + + public class EventAggregatorTestNotificationHandler : INotificationHandler + { + private readonly EventAggregatorTestScopedService _scopedService; + + public EventAggregatorTestNotificationHandler(EventAggregatorTestScopedService scopedService) => _scopedService = scopedService; + + // Mutation proves that the handler runs despite depending on scoped service. + public void Handle(EventAggregatorTestNotification notification) => notification.Mutated = true; + } +} From d9bdd8196c7e06d50411205ac4f1178ff69b6d60 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 10 Jan 2022 16:03:33 +0000 Subject: [PATCH 16/23] Misc/obsolete redundant extension (#11838) * Mark AddUnique obsolete. * Remove all internal usages of AddUnique. --- .../ServiceCollectionExtensions.cs | 2 ++ .../DependencyInjection/UmbracoBuilder.cs | 24 +++++++++---------- .../UmbracoBuilder.Examine.cs | 2 +- .../UmbracoBuilder.FileSystems.cs | 2 +- .../UmbracoBuilder.Installer.cs | 2 +- .../UmbracoBuilder.Services.cs | 8 +++---- .../UmbracoBuilderExtensions.cs | 12 +++++----- .../UmbracoBuilderExtensions.cs | 18 +++++++------- 8 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs index cec1cbb4eb..871a0bbe02 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -24,6 +24,8 @@ namespace Umbraco.Extensions services.AddUnique(factory => (TImplementing)factory.GetRequiredService()); } + // TODO(V11): Remove this function. + [Obsolete("This method is functionally equivalent to AddSingleton() please use that instead.")] public static void AddUnique(this IServiceCollection services) where TImplementing : class => services.Replace(ServiceDescriptor.Singleton()); diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 1af25e16e8..eacd615830 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -171,7 +171,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - Services.AddUnique(); + Services.AddSingleton(); // by default, register a noop factory Services.AddUnique(); @@ -179,7 +179,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddSingleton(f => f.GetRequiredService().CreateDictionary()); - Services.AddUnique(); + Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); @@ -194,14 +194,14 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); // register properties fallback Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); // register published router Services.AddUnique(); @@ -226,13 +226,13 @@ namespace Umbraco.Cms.Core.DependencyInjection // let's use an hybrid accessor that can fall back to a ThreadStatic context. Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); // register a server registrar, by default it's the db registrar Services.AddUnique(f => diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index d061a4372c..da31a8df39 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection false)); builder.Services.AddUnique, MediaValueSetBuilder>(); builder.Services.AddUnique, MemberValueSetBuilder>(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index f66991fb68..ccb515182e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -34,7 +34,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) { // register FileSystems, which manages all filesystems - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the scheme for media paths builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index 990b158ef3..e0958bfdb7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddScoped(); builder.Services.AddTransient(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index debe476c49..7d74ff13c8 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -30,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) { // register the service context - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the special idk map builder.Services.AddUnique(); @@ -74,11 +74,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); return builder; diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 7a15d640e6..2c801e963b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -82,12 +82,12 @@ namespace Umbraco.Extensions { builder.Services.AddSingleton(); builder.Services.ConfigureOptions(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.AddNotificationAsyncHandler(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -115,7 +115,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 97a9f3e087..247e364ba4 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -148,7 +148,7 @@ namespace Umbraco.Extensions // Add supported databases builder.AddUmbracoSqlServerSupport(); builder.AddUmbracoSqlCeSupport(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( @@ -200,7 +200,7 @@ namespace Umbraco.Extensions ///
public static IUmbracoBuilder AddUmbracoProfiler(this IUmbracoBuilder builder) { - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddMiniProfiler(options => { @@ -286,7 +286,7 @@ namespace Umbraco.Extensions builder.Services.AddSmidgeInMemory(false); // it will be enabled based on config/cachebuster builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.ConfigureOptions(); @@ -332,7 +332,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the umbraco context factory @@ -343,12 +343,12 @@ namespace Umbraco.Extensions builder.WithCollectionBuilder() .Add(umbracoApiControllerTypes); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique(); From 18a3133536ffed06c23c78890e468f337f2215a0 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 12 Jan 2022 12:32:53 +0100 Subject: [PATCH 17/23] v9: Fix max allowed content length on iis & kestrel (#11802) * Update web.config * Change web.config to align with v8 default values * Adjust kestrel options to align with v8 * Add better comment * added web.config to root * change web.config to 30mb * delete obsolete comment * No reason to have web.config to just have it default * Add back ConfigureIISServerOptions.cs * Add obsolete comment, can't link to documentation yet as it doesn't exist * Apply suggestions from code review Co-authored-by: Mole * Add link to documentation Co-authored-by: Mole --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 1 - .../Security/ConfigureIISServerOptions.cs | 10 ++++++++-- .../Security/ConfigureKestrelServerOptions.cs | 4 ++-- src/Umbraco.Web.UI.Client/src/web.config | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 247e364ba4..819076bbb8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -109,7 +109,6 @@ namespace Umbraco.Extensions var appCaches = AppCaches.Create(requestCache); services.ConfigureOptions(); - services.ConfigureOptions(); services.ConfigureOptions(); IProfiler profiler = GetWebProfiler(config); diff --git a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs index 4141669c1c..1c1460432f 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs @@ -1,18 +1,24 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Web.Common.Security { + [Obsolete("This class is obsolete, as this does not configure your Maximum request length, see https://our.umbraco.com/documentation/Reference/V9-Config/MaximumUploadSizeSettings/ for information about configuring maximum request length")] public class ConfigureIISServerOptions : IConfigureOptions { private readonly IOptions _runtimeSettings; - public ConfigureIISServerOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; + public ConfigureIISServerOptions(IOptions runtimeSettings) => + _runtimeSettings = runtimeSettings; + public void Configure(IISServerOptions options) { // convert from KB to bytes - options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. + options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue + ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 + : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. } } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs index c11e0d8814..59ec330700 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs @@ -11,8 +11,8 @@ namespace Umbraco.Cms.Web.Common.Security public ConfigureKestrelServerOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; public void Configure(KestrelServerOptions options) { - // convert from KB to bytes - options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : long.MaxValue; + // convert from KB to bytes, 52428800 bytes (50 MB) is the same as in the IIS settings + options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : 52428800; } } } diff --git a/src/Umbraco.Web.UI.Client/src/web.config b/src/Umbraco.Web.UI.Client/src/web.config index 6903c39608..6d14a9bab7 100644 --- a/src/Umbraco.Web.UI.Client/src/web.config +++ b/src/Umbraco.Web.UI.Client/src/web.config @@ -5,4 +5,4 @@ - \ No newline at end of file + From baba7ffed9e59362c50fa9b28e286dbe38b430cb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 12 Jan 2022 13:54:14 +0100 Subject: [PATCH 18/23] Bugfix - Take ufprt from form data if the request has form content type, otherwise fallback to use the query (#11845) --- .../Routing/UmbracoRouteValueTransformer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 717a6b490a..9106c3ed09 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -130,7 +130,7 @@ namespace Umbraco.Cms.Web.Website.Routing IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext); - umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); + umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); // now we need to do some public access checks umbracoRouteValues = await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues); @@ -202,8 +202,8 @@ namespace Umbraco.Cms.Web.Website.Routing } // if it is a POST/GET then a value must be in the request - if (!httpContext.Request.Query.TryGetValue("ufprt", out StringValues encodedVal) - && (!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out encodedVal))) + if ((!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out StringValues encodedVal)) + && !httpContext.Request.Query.TryGetValue("ufprt", out encodedVal)) { return null; } From 2c44d676860dad2aa552e755f5b444b46b2c3abd Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 17 Jan 2022 14:42:41 +0000 Subject: [PATCH 19/23] Modify codeql setup. (#11876) --- .github/config/codeql-config.yml | 14 -------- .github/workflows/codeql-analysis.yml | 46 ++------------------------- 2 files changed, 3 insertions(+), 57 deletions(-) delete mode 100644 .github/config/codeql-config.yml diff --git a/.github/config/codeql-config.yml b/.github/config/codeql-config.yml deleted file mode 100644 index 7bac345491..0000000000 --- a/.github/config/codeql-config.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "CodeQL config" -on: - push: - branches: [v8/contrib,v8/dev] -paths-ignore: - - node_modules - - Umbraco.TestData - - Umbraco.Tests - - Umbraco.Tests.AcceptanceTest - - Umbraco.Tests.Benchmarks - - bin - - build.tmp -paths: - - src diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ee912262d7..c30bd4b44f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,11 +2,10 @@ name: "Code scanning - action" on: push: - branches: [v8/contrib,v8/dev,v8/bug,v8/feature] + branches: ['*/dev','*/contrib', '*/feature/**'] pull_request: # The branches below must be a subset of the branches above - schedule: - - cron: '0 7 * * 2' + branches: ['*/dev','*/contrib'] jobs: CodeQL-Build: @@ -16,53 +15,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - - name: configure Pagefile - uses: al-cheb/configure-pagefile-action@v1.2 - with: - minimum-size: 8GB - maximum-size: 32GB - + - run: | echo "Run Umbraco-CMS build" pwsh -command .\build\build.ps1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 - with: - config-file: ./.github/config/codeql-config.yml - - # This job is to prevent the workflow status from showing as failed when all other jobs are skipped - See https://github.community/t/workflow-is-failing-if-no-job-can-be-ran-due-to-condition/16873 - always_job: - name: Always run job - runs-on: ubuntu-latest - steps: - - name: Always run - run: echo "This job is to prevent the workflow status from showing as failed when all other jobs are skipped" - From c410f789860db0fd89267a619e442b6730eff8bf Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 17 Jan 2022 21:41:24 +0000 Subject: [PATCH 20/23] Restore config file and prevent duplicate run for feature branches --- .github/config/codeql-config.yml | 4 ++++ .github/workflows/codeql-analysis.yml | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .github/config/codeql-config.yml diff --git a/.github/config/codeql-config.yml b/.github/config/codeql-config.yml new file mode 100644 index 0000000000..dd94726dba --- /dev/null +++ b/.github/config/codeql-config.yml @@ -0,0 +1,4 @@ +name: "CodeQL config" + +paths: + - src diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c30bd4b44f..9138fc4a20 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "Code scanning - action" on: push: - branches: ['*/dev','*/contrib', '*/feature/**'] + branches: ['*/dev','*/contrib'] pull_request: # The branches below must be a subset of the branches above branches: ['*/dev','*/contrib'] @@ -26,3 +26,5 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 + with: + config-file: ./.github/config/codeql-config.yml \ No newline at end of file From 9a145537aae0220bbbfe7bb895456c12deff3b73 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 17 Jan 2022 22:29:42 +0000 Subject: [PATCH 21/23] Fix config file, should be on init --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9138fc4a20..d3575421f8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -19,12 +19,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 - + with: + config-file: ./.github/config/codeql-config.yml + - run: | echo "Run Umbraco-CMS build" pwsh -command .\build\build.ps1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 - with: - config-file: ./.github/config/codeql-config.yml \ No newline at end of file From 576f90ad37569bafd5db73ad8fab29432e57bd56 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 18 Jan 2022 00:35:53 +0000 Subject: [PATCH 22/23] Speedup codeql run (#11877) * Speedup codeql run * npm build once, ignore node_modules * Misc cleanup --- .github/config/codeql-config.yml | 3 +++ .github/workflows/codeql-analysis.yml | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/config/codeql-config.yml b/.github/config/codeql-config.yml index dd94726dba..432be995a5 100644 --- a/.github/config/codeql-config.yml +++ b/.github/config/codeql-config.yml @@ -2,3 +2,6 @@ name: "CodeQL config" paths: - src + +paths-ignore: + - '**/node_modules' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d3575421f8..60292dba45 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,7 +10,7 @@ on: jobs: CodeQL-Build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout repository @@ -22,9 +22,8 @@ jobs: with: config-file: ./.github/config/codeql-config.yml - - run: | - echo "Run Umbraco-CMS build" - pwsh -command .\build\build.ps1 + - name: Build + run: dotnet build umbraco-netcore-only.sln # also runs npm build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 From bf166225def87b80a25e11244cb5868a3ca2d43e Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 18 Jan 2022 10:39:22 +0000 Subject: [PATCH 23/23] Move Umbraco.Tests to legacy dir. (#11878) (makes grep easier) --- {tests => legacy}/Umbraco.Tests/App.config | 0 {tests => legacy}/Umbraco.Tests/Properties/AssemblyInfo.cs | 0 {tests => legacy}/Umbraco.Tests/Published/ModelTypeTests.cs | 0 {tests => legacy}/Umbraco.Tests/Routing/BaseUrlProviderTest.cs | 0 {tests => legacy}/Umbraco.Tests/Routing/MediaUrlProviderTests.cs | 0 {tests => legacy}/Umbraco.Tests/Umbraco.Tests.csproj | 0 .../Web/Controllers/AuthenticationControllerTests.cs | 0 {tests => legacy}/Umbraco.Tests/unit-test-log4net.CI.config | 0 {tests => legacy}/Umbraco.Tests/unit-test-logger.config | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {tests => legacy}/Umbraco.Tests/App.config (100%) rename {tests => legacy}/Umbraco.Tests/Properties/AssemblyInfo.cs (100%) rename {tests => legacy}/Umbraco.Tests/Published/ModelTypeTests.cs (100%) rename {tests => legacy}/Umbraco.Tests/Routing/BaseUrlProviderTest.cs (100%) rename {tests => legacy}/Umbraco.Tests/Routing/MediaUrlProviderTests.cs (100%) rename {tests => legacy}/Umbraco.Tests/Umbraco.Tests.csproj (100%) rename {tests => legacy}/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs (100%) rename {tests => legacy}/Umbraco.Tests/unit-test-log4net.CI.config (100%) rename {tests => legacy}/Umbraco.Tests/unit-test-logger.config (100%) diff --git a/tests/Umbraco.Tests/App.config b/legacy/Umbraco.Tests/App.config similarity index 100% rename from tests/Umbraco.Tests/App.config rename to legacy/Umbraco.Tests/App.config diff --git a/tests/Umbraco.Tests/Properties/AssemblyInfo.cs b/legacy/Umbraco.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from tests/Umbraco.Tests/Properties/AssemblyInfo.cs rename to legacy/Umbraco.Tests/Properties/AssemblyInfo.cs diff --git a/tests/Umbraco.Tests/Published/ModelTypeTests.cs b/legacy/Umbraco.Tests/Published/ModelTypeTests.cs similarity index 100% rename from tests/Umbraco.Tests/Published/ModelTypeTests.cs rename to legacy/Umbraco.Tests/Published/ModelTypeTests.cs diff --git a/tests/Umbraco.Tests/Routing/BaseUrlProviderTest.cs b/legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs similarity index 100% rename from tests/Umbraco.Tests/Routing/BaseUrlProviderTest.cs rename to legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs diff --git a/tests/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs similarity index 100% rename from tests/Umbraco.Tests/Routing/MediaUrlProviderTests.cs rename to legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs diff --git a/tests/Umbraco.Tests/Umbraco.Tests.csproj b/legacy/Umbraco.Tests/Umbraco.Tests.csproj similarity index 100% rename from tests/Umbraco.Tests/Umbraco.Tests.csproj rename to legacy/Umbraco.Tests/Umbraco.Tests.csproj diff --git a/tests/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs similarity index 100% rename from tests/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs rename to legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs diff --git a/tests/Umbraco.Tests/unit-test-log4net.CI.config b/legacy/Umbraco.Tests/unit-test-log4net.CI.config similarity index 100% rename from tests/Umbraco.Tests/unit-test-log4net.CI.config rename to legacy/Umbraco.Tests/unit-test-log4net.CI.config diff --git a/tests/Umbraco.Tests/unit-test-logger.config b/legacy/Umbraco.Tests/unit-test-logger.config similarity index 100% rename from tests/Umbraco.Tests/unit-test-logger.config rename to legacy/Umbraco.Tests/unit-test-logger.config