diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs index 74ce67ed03..219ce30e78 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs @@ -2,14 +2,19 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Extensions; using Umbraco.New.Cms.Web.Common.Routing; +using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult; +using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace Umbraco.Cms.ManagementApi.Controllers.Security; @@ -21,12 +26,18 @@ public class BackOfficeController : ManagementApiControllerBase private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBackOfficeSignInManager _backOfficeSignInManager; private readonly IBackOfficeUserManager _backOfficeUserManager; + private readonly IOptions _securitySettings; - public BackOfficeController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager) + public BackOfficeController( + IHttpContextAccessor httpContextAccessor, + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager, + IOptions securitySettings) { _httpContextAccessor = httpContextAccessor; _backOfficeSignInManager = backOfficeSignInManager; _backOfficeUserManager = backOfficeUserManager; + _securitySettings = securitySettings; } [HttpGet("authorize")] @@ -41,36 +52,93 @@ public class BackOfficeController : ManagementApiControllerBase return BadRequest("Unable to obtain OpenID data from the current request"); } + return request.IdentityProvider.IsNullOrWhiteSpace() + ? await AuthorizeInternal(request) + : await AuthorizeExternal(request); + } + + private async Task AuthorizeInternal(OpenIddictRequest request) + { + // TODO: ensure we handle sign-in notifications for internal logins. + // when the new login screen is implemented for internal logins, make sure it still handles + // user sign-in notifications (calls BackOfficeSignInManager.HandleSignIn) as part of the + // sign-in process + // for future reference, notifications are already handled for the external login flow by + // by calling BackOfficeSignInManager.ExternalLoginSignInAsync + // retrieve the user principal stored in the authentication cookie. AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - if (cookieAuthResult.Succeeded && cookieAuthResult.Principal?.Identity?.Name != null) + var userName = cookieAuthResult.Succeeded + ? cookieAuthResult.Principal?.Identity?.Name + : null; + + if (userName != null) { - BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(cookieAuthResult.Principal.Identity.Name); + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(userName); if (backOfficeUser != null) { - ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); - backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); - - // TODO: it is not optimal to append all claims to the token. - // the token size grows with each claim, although it is still smaller than the old cookie. - // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. - // maybe work with scopes instead? - Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); - foreach (Claim backOfficeClaim in backOfficeClaims) - { - backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); - } - - if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) - { - // "offline_access" scope is required to use refresh tokens - backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); - } - - return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + return await SignInBackOfficeUser(backOfficeUser, request); } } - return new ChallengeResult(new[] { Constants.Security.BackOfficeAuthenticationType }); + return DefaultChallengeResult(); } + + private async Task AuthorizeExternal(OpenIddictRequest request) + { + var provider = request.IdentityProvider ?? throw new ArgumentException("No identity provider found in request", nameof(request)); + + ExternalLoginInfo? loginInfo = await _backOfficeSignInManager.GetExternalLoginInfoAsync(); + if (loginInfo?.Principal != null) + { + IdentitySignInResult result = await _backOfficeSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.UserBypassTwoFactorForExternalLogins); + + if (result.Succeeded) + { + // Update any authentication tokens if succeeded + await _backOfficeSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); + + // sign in the backoffice user associated with the login provider and unique provider key + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (backOfficeUser != null) + { + return await SignInBackOfficeUser(backOfficeUser, request); + } + } + else + { + // avoid infinite auth loops when something fails by performing the default challenge (default login screen) + return DefaultChallengeResult(); + } + } + + AuthenticationProperties properties = _backOfficeSignInManager.ConfigureExternalAuthenticationProperties(provider, null); + return new ChallengeResult(provider, properties); + } + + private async Task SignInBackOfficeUser(BackOfficeIdentityUser backOfficeUser, OpenIddictRequest request) + { + ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); + backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); + + // TODO: it is not optimal to append all claims to the token. + // the token size grows with each claim, although it is still smaller than the old cookie. + // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. + // maybe work with scopes instead? + Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); + foreach (Claim backOfficeClaim in backOfficeClaims) + { + backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } + + if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) + { + // "offline_access" scope is required to use refresh tokens + backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); + } + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + } + + private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index 7d477069ed..24d17d34a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs index 54d6063ad7..c666547c1d 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -69,7 +69,9 @@ public class SqliteDatabaseCreator : IDatabaseCreator * always initializing in this way and it probably helps for non azure scenarios also (anytime persisting on a cifs mount for example). */ - var tempFile = Path.GetTempFileName(); + // Create a random file name using cryptographically strong random number generator (RNGCryptoServiceProvider) + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempConnectionString = new SqliteConnectionStringBuilder { DataSource = tempFile, Pooling = false }; using (var connection = new SqliteConnection(tempConnectionString.ConnectionString)) diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index 056eb09513..922be06acc 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -50,4 +50,12 @@ + + + + + + + + diff --git a/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props b/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props index 6168686bcb..b79d9f9a4b 100644 --- a/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props +++ b/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props @@ -5,7 +5,7 @@ $(DefaultItemExcludes);umbraco\Logs\** $(DefaultItemExcludes);wwwroot\media\** - + diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index e1a0e2665c..1ff6e848ad 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -1,4 +1,4 @@ - + Umbraco CMS Installs Umbraco CMS with all default dependencies in your ASP.NET Core project. @@ -12,4 +12,12 @@ + + + + + + + + diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 4014930a5c..5a82d860bb 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -152,7 +152,7 @@ public class ContentSettings internal const string StaticMacroErrors = "Inline"; internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; internal const bool StaticShowDeprecatedPropertyEditors = false; - internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; + internal const string StaticLoginBackgroundImage = "assets/img/login.svg"; internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; internal const bool StaticHideBackOfficeLogo = false; internal const bool StaticDisableDeleteWhenReferenced = false; diff --git a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs index 37b671926c..90498991a4 100644 --- a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using System.ComponentModel; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.Extensions; namespace Umbraco.Cms.Core.Configuration.Models; @@ -12,10 +14,28 @@ namespace Umbraco.Cms.Core.Configuration.Models; public class LoggingSettings { internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); + internal const string StaticDirectory = Constants.SystemDirectories.LogFiles; /// /// Gets or sets a value for the maximum age of a log file. /// [DefaultValue(StaticMaxLogAge)] public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); + + /// + /// Gets or sets the folder to use for log files + /// + [DefaultValue(StaticDirectory)] + public string Directory { get; set; } = StaticDirectory; + + public string GetAbsoluteLoggingPath(IHostEnvironment hostEnvironment) + { + var dir = Directory; + if (dir.StartsWith("~/")) + { + return hostEnvironment.MapPathContentRoot(dir); + } + + return dir; + } } diff --git a/src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs b/src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs index 092e420e3c..73f7da185f 100644 --- a/src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs @@ -12,5 +12,5 @@ public class MarketplaceSettings /// /// Gets or sets the additional parameters that are sent to the Marketplace. /// - public IDictionary AdditionalParameters { get; set; } = new Dictionary(); + public Dictionary AdditionalParameters { get; set; } = new (); } diff --git a/src/Umbraco.Core/Constants-Marketplace.cs b/src/Umbraco.Core/Constants-Marketplace.cs index 6d5abb38e7..38cf282b74 100644 --- a/src/Umbraco.Core/Constants-Marketplace.cs +++ b/src/Umbraco.Core/Constants-Marketplace.cs @@ -7,6 +7,6 @@ public static partial class Constants /// public static class Marketplace { - public const string Url = "https://dev.marketplace.umbraco.com"; + public const string Url = "https://marketplace.umbraco.com"; } } diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index 85375390ac..d0e4488e0e 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -60,6 +60,7 @@ public static partial class Constants /// /// The default folder where Umbraco log files are stored /// + [Obsolete("Use LoggingSettings.GetLoggingDirectory() instead, will be removed in Umbraco 13.")] public const string LogFiles = Umbraco + "/Logs"; [Obsolete("Use PluginIcons instead")] diff --git a/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs b/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs new file mode 100644 index 0000000000..dbd501d277 --- /dev/null +++ b/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs @@ -0,0 +1,48 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class DataTypeConfigurationConnectorExtensions +{ + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The connector. + /// The data type. + /// The dependencies. + /// The context cache. + /// + /// The artifact configuration value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? ToArtifact(this IDataTypeConfigurationConnector connector, IDataType dataType, ICollection dependencies, IContextCache contextCache) + => connector is IDataTypeConfigurationConnector2 connector2 + ? connector2.ToArtifact(dataType, dependencies, contextCache) + : connector.ToArtifact(dataType, dependencies); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The connector. + /// The data type. + /// The artifact configuration value. + /// The context cache. + /// + /// The data type configuration. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static object? FromArtifact(this IDataTypeConfigurationConnector connector, IDataType dataType, string? configuration, IContextCache contextCache) + => connector is IDataTypeConfigurationConnector2 connector2 + ? connector2.FromArtifact(dataType, configuration, contextCache) + : connector.FromArtifact(dataType, configuration); +} diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs index 506d1f5745..36302efd07 100644 --- a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs @@ -27,20 +27,8 @@ public interface IDataTypeConfigurationConnector /// /// The artifact configuration value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? ToArtifact(IDataType dataType, ICollection dependencies) - => ToArtifact(dataType, dependencies, PassThroughCache.Instance); - - /// - /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. - /// - /// The data type. - /// The dependencies. - /// The context cache. - /// - /// The artifact configuration value. - /// - string? ToArtifact(IDataType dataType, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IDataTypeConfigurationConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? ToArtifact(IDataType dataType, ICollection dependencies); /// /// Gets the data type configuration corresponding to an artifact configuration value. @@ -50,18 +38,6 @@ public interface IDataTypeConfigurationConnector /// /// The data type configuration. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - object? FromArtifact(IDataType dataType, string? configuration) - => FromArtifact(dataType, configuration, PassThroughCache.Instance); - - /// - /// Gets the data type configuration corresponding to an artifact configuration value. - /// - /// The data type. - /// The artifact configuration value. - /// The context cache. - /// - /// The data type configuration. - /// - object? FromArtifact(IDataType dataType, string? configuration, IContextCache contextCache); + [Obsolete($"Implement {nameof(IDataTypeConfigurationConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? FromArtifact(IDataType dataType, string? configuration); } diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs new file mode 100644 index 0000000000..772bc35dc4 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs @@ -0,0 +1,56 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IDataTypeConfigurationConnector2 : IDataTypeConfigurationConnector +{ + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The data type. + /// The dependencies. + /// + /// The artifact configuration value. + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IDataTypeConfigurationConnector.ToArtifact(IDataType dataType, ICollection dependencies) + => ToArtifact(dataType, dependencies, PassThroughCache.Instance); + + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The data type. + /// The dependencies. + /// The context cache. + /// + /// The artifact configuration value. + /// + string? ToArtifact(IDataType dataType, ICollection dependencies, IContextCache contextCache); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The data type. + /// The artifact configuration value. + /// + /// The data type configuration. + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? IDataTypeConfigurationConnector.FromArtifact(IDataType dataType, string? configuration) + => FromArtifact(dataType, configuration, PassThroughCache.Instance); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The data type. + /// The artifact configuration value. + /// The context cache. + /// + /// The data type configuration. + /// + object? FromArtifact(IDataType dataType, string? configuration, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs index b4f530fb35..adf4c57502 100644 --- a/src/Umbraco.Core/Deploy/IServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -15,19 +15,8 @@ public interface IServiceConnector : IDiscoverable /// /// The corresponding artifact, or null. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - IArtifact? GetArtifact(Udi udi) - => GetArtifact(udi, PassThroughCache.Instance); - - /// - /// Gets an artifact. - /// - /// The entity identifier of the artifact. - /// The context cache. - /// - /// The corresponding artifact, or null. - /// - IArtifact? GetArtifact(Udi udi, IContextCache contextCache); + [Obsolete($"Implement {nameof(IServiceConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact? GetArtifact(Udi udi); /// /// Gets an artifact. @@ -36,19 +25,8 @@ public interface IServiceConnector : IDiscoverable /// /// The corresponding artifact. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - IArtifact GetArtifact(object entity) - => GetArtifact(entity, PassThroughCache.Instance); - - /// - /// Gets an artifact. - /// - /// The entity. - /// The context cache. - /// - /// The corresponding artifact. - /// - IArtifact GetArtifact(object entity, IContextCache contextCache); + [Obsolete($"Implement {nameof(IServiceConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact GetArtifact(object entity); /// /// Initializes processing for an artifact. diff --git a/src/Umbraco.Core/Deploy/IServiceConnector2.cs b/src/Umbraco.Core/Deploy/IServiceConnector2.cs new file mode 100644 index 0000000000..6c1558a956 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IServiceConnector2.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IServiceConnector2 : IServiceConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact? IServiceConnector.GetArtifact(Udi udi) + => GetArtifact(udi, PassThroughCache.Instance); + + /// + /// Gets an artifact. + /// + /// The entity identifier of the artifact. + /// The context cache. + /// + /// The corresponding artifact, or null. + /// + IArtifact? GetArtifact(Udi udi, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact IServiceConnector.GetArtifact(object entity) + => GetArtifact(entity, PassThroughCache.Instance); + + /// + /// Gets an artifact. + /// + /// The entity. + /// The context cache. + /// + /// The corresponding artifact. + /// + IArtifact GetArtifact(object entity, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 9f3b17f71c..fe28e41017 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -29,21 +29,8 @@ public interface IValueConnector /// /// The deploy property value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies) - => ToArtifact(value, propertyType, dependencies, PassThroughCache.Instance); - - /// - /// Gets the deploy property value corresponding to a content property value, and gather dependencies. - /// - /// The content property value. - /// The value property type - /// The content dependencies. - /// The context cache. - /// - /// The deploy property value. - /// - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); /// /// Gets the content property value corresponding to a deploy property value. @@ -54,19 +41,6 @@ public interface IValueConnector /// /// The content property value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue) - => FromArtifact(value, propertyType, currentValue, PassThroughCache.Instance); - - /// - /// Gets the content property value corresponding to a deploy property value. - /// - /// The deploy property value. - /// The value property type - /// The current content property value. - /// The context cache. - /// - /// The content property value. - /// - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache); + [Obsolete($"Implement {nameof(IValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); } diff --git a/src/Umbraco.Core/Deploy/IValueConnector2.cs b/src/Umbraco.Core/Deploy/IValueConnector2.cs new file mode 100644 index 0000000000..a0c99dca06 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IValueConnector2.cs @@ -0,0 +1,44 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IValueConnector2 : IValueConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IValueConnector.ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies) + => ToArtifact(value, propertyType, dependencies, PassThroughCache.Instance); + + /// + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. + /// + /// The content property value. + /// The value property type + /// The content dependencies. + /// The context cache. + /// + /// The deploy property value. + /// + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? IValueConnector.FromArtifact(string? value, IPropertyType propertyType, object? currentValue) + => FromArtifact(value, propertyType, currentValue, PassThroughCache.Instance); + + /// + /// Gets the content property value corresponding to a deploy property value. + /// + /// The deploy property value. + /// The value property type + /// The current content property value. + /// The context cache. + /// + /// The content property value. + /// + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs b/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs new file mode 100644 index 0000000000..0d0000f97c --- /dev/null +++ b/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs @@ -0,0 +1,44 @@ +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class ServiceConnectorExtensions +{ + /// + /// Gets an artifact. + /// + /// The connector. + /// The entity identifier of the artifact. + /// The context cache. + /// + /// The corresponding artifact, or null. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static IArtifact? GetArtifact(this IServiceConnector connector, Udi udi, IContextCache contextCache) + => connector is IServiceConnector2 connector2 + ? connector2.GetArtifact(udi, contextCache) + : connector.GetArtifact(udi); + + /// + /// Gets an artifact. + /// + /// The connector. + /// The entity. + /// The context cache. + /// + /// The corresponding artifact. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static IArtifact GetArtifact(this IServiceConnector connector, object entity, IContextCache contextCache) + => connector is IServiceConnector2 connector2 + ? connector2.GetArtifact(entity, contextCache) + : connector.GetArtifact(entity); +} diff --git a/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs b/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs new file mode 100644 index 0000000000..eadcee55e0 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs @@ -0,0 +1,50 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class ValueConnectorExtensions +{ + /// + /// Gets the artifact value corresponding to a property value and gather dependencies. + /// + /// The connector. + /// The property value. + /// The property type. + /// The dependencies. + /// The context cache. + /// + /// The artifact value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? ToArtifact(this IValueConnector connector, object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache) + => connector is IValueConnector2 connector2 + ? connector2.ToArtifact(value, propertyType, dependencies, contextCache) + : connector.ToArtifact(value, propertyType, dependencies); + + /// + /// Gets the property value corresponding to an artifact value. + /// + /// The connector. + /// The artifact value. + /// The property type. + /// The current property value. + /// The context cache. + /// + /// The property value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static object? FromArtifact(this IValueConnector connector, string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache) + => connector is IValueConnector2 connector2 + ? connector2.FromArtifact(value, propertyType, currentValue, contextCache) + : connector.FromArtifact(value, propertyType, currentValue); +} diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml index dab49ce73b..09c679475d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml @@ -1194,7 +1194,6 @@ Vyberte verzi, kterou chcete porovnat s aktuální verzí - Současná verze Červený text nebude ve vybrané verzi zobrazen, zelený znamená přidaný].]]> Dokument byl vrácen na starší verzi Tohle zobrazuje vybranou verzi jako html, jestliže chcete vidět rozdíly mezi 2 verzemi najednou, použijte rozdílové zobrazení diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index a41ba22abc..836d4705b3 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -1589,8 +1589,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Dewis fersiwn i gymharu efo fersiwn bresennol Newidiadau - Creuwyd - Fersiwn bresennol Ni fydd testun coch yn cael ei ddangos yn y fersiwn dewiswyd. , mae gwyrdd yn golygu wedi'i ychwanegu]]> Dogfen wedi'i rolio yn ôl Mae hyn yn dangos y fersiwn dewiswyd ar ffurf HTML, os hoffwch weld y gwahaniaeth rhwng 2 fersiwn ar yr un pryd, defnyddiwch y wedd gwahaniaethol diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 624c343d72..0555ea3934 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1238,11 +1238,10 @@ Mange hilsner fra Umbraco robotten Ændringer - Oprettet Vælg en version at sammenligne med den nuværende version - Nuværende version Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> + Der er ingen forskelle mellem den nuværende version og den valgte version Dokument tilbagerullet Her vises den valgte version som html. Hvis du ønsker at se forskellen mellem de 2 versioner på samme tid, brug 'diff'-oversigten diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/de.xml b/src/Umbraco.Core/EmbeddedResources/Lang/de.xml index 0400ca030e..017e56602e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/de.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/de.xml @@ -1273,7 +1273,6 @@ Wählen Sie eine Version, um diese mit der aktuellen zu vergleichen - Aktuelle Version Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. Dokument wurde zurückgesetzt Zeigt die ausgewählte Version als HTML an. Wenn Sie sich die Unterschiede zwischen zwei Versionen anzeigen lassen wollen, benutzen Sie bitte die Vergleichsansicht. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index b07b0aa482..d568f48102 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -346,6 +346,7 @@ Create a new member All Members Member groups have no additional properties for editing. + Two-Factor Authentication Failed to copy content type @@ -1455,7 +1456,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Created Current version - Red text will be removed in the selected version, green text will be added]]> + Red text will be removed in the selected version, green text will be added]]> + There are no differences between the current (draft) version and the selected version Document has been rolled back Select a version to compare with the current version This displays the selected version as HTML, if you wish to see the difference between 2 @@ -2792,7 +2794,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Allow in root Make this block available in the root of the layout. Allow in areas - Make this block available for use within the areas of other Blocks (unless explicit permissions are set for these areas). + Make this block available by default within the areas of other Blocks (unless explicit permissions are set for these areas). By default, all block types are allowed in an Area, Use this option to allow only selected types. Areas Grid Columns for Areas diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index ca6a81ffc1..5acf1b7abe 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -362,6 +362,7 @@ The member already has a password set Lockout is not enabled for this member The member is not in group '%0%' + Two-Factor Authentication Failed to copy content type @@ -1496,7 +1497,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Select a version to compare with the current version Current version - Red text will be removed in the selected version, green text will be added]]> + Red text will be removed in the selected version, green text will be added]]> + There are no differences between the current (draft) version and the selected version Document has been rolled back This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view @@ -2895,7 +2897,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Allow in root Make this block available in the root of the layout. Allow in areas - Make this block available for use within the areas of other Blocks (unless explicit permissions are set for these areas). + Make this block available by default within the areas of other Blocks (unless explicit permissions are set for these areas). By default, all block types are allowed in an Area, Use this option to allow only selected types. Areas Grid Columns for Areas diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 6b3bcd7637..b2ab390a2e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -898,7 +898,6 @@ Reiniciar - Versión actual Red el texto de la versión seleccionada no se mostrará. , el verde significa añadido]]> Se ha recuperado la última versión del documento. Esto muestra la versión seleccionada como html, si deseas ver la diferencia entre 2 versiones al mismo tiempo, por favor usa la vista diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml index c2bb341f06..1576cedbe4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml @@ -1251,7 +1251,6 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Sélectionnez une version à comparer avec la version actuelle - Version actuelle Le texte en Rouge signifie qu'il a été supprimé de la version choisie, vert signifie ajouté]]> Le document a été restauré à une version antérieure Ceci affiche la version choisie en tant que HTML, si vous souhaitez voir les différences entre les deux versions en même temps, utilisez la vue différentielle diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml index 7f75cb1dc5..f0427bbae1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml @@ -589,7 +589,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont הסר קישור - גירסה עדכנית טקסט אדום לא יוצג בגרסא שנבחרה, טקסט ירוק מייצט טקסט שנוסף.]]> המסמך שוחזר בהצלחה להלן הגרסא שנבחרה כHTML, אם הינך לצפות בשינויים בין שתי הגרסאות בו זמנית, בחר ב diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml index 49402c693b..2430542e66 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml @@ -1530,9 +1530,7 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in Modifiche - Creato Seleziona una versione da confrontare con la versione corrente - Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml index 5f7847ed58..9a94e7d924 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml @@ -730,7 +730,6 @@ Runwayをインストールして作られた新しいウェブサイトがど リセット - 現在の版 の文字列は以前の版にはない部分で、緑の文字列は以前の版にのみある部分です。]]> ドキュメントは以前の版に戻りました 選択した版をhtmlで表示します。2つの版の比較を表示したいときは、Diff を選択してください。 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml index 3467a2b170..4aa0d4ad89 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml @@ -575,7 +575,6 @@ 링크 삭제 - 현재 버전 빨간 텍스트는 선택한 버전에선 보이지 않습니다. 녹색은 추가되었음을 의미합니다]]> 문서가 롤백되었습니다. 선택한 버전을 html로 보여줍니다. 두 버전의 차이점을 동시에 보시려면, 차이점 보기를 사용하세요 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml index 7fa544b41c..c2784b3a9c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml @@ -621,7 +621,6 @@ Vennlig hilsen Umbraco roboten Nullstill - Gjeldende versjon Rød tekst vil ikke bli vist i den valgte versjonen. , grønn betyr lagt til]]> Dokumentet er tilbakeført til en tidligere versjon Dette viser den valgte versjonen som HTML, bruk avviksvisningen hvis du ønsker å se forksjellene mellom to versjoner samtidig. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml index facae05dae..53aa716792 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml @@ -881,7 +881,6 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Resetuj - Aktualna wersja Czerwony tekst nie będzie pokazany w wybranej wersji, zielony tekst został dodany]]> Dokument został przywrócony Tu widać wybraną wersję jako html, jeżeli chcesz zobaczyć różnicę pomiędzy 2 wersjami w tym samym czasie, użyj podglądu różnic diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml index ecc7274ca6..f6cdb21208 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml @@ -565,7 +565,6 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Remover Link - Versão atual Texto vermelho não será mostrado na versão selecionada; verde significa adicionado]]> Documento foi revertido Isto mostra a versão selecionada como html se você deseja ver as diferenças entre as 2 versões ao mesmo tempo use a visão em diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml index 4b283d4311..39be7570a3 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml @@ -1195,7 +1195,6 @@ '%0%' была переименована в '%1%' - Текущая версия Красным отмечен текст, которого уже нет в последней версии, зеленым - текст, который добавлен]]> Произведен откат к ранней версии Текущая версия показана в виде HTML. Для просмотра различий в версиях выберите режим сравнения diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml index 4bfed67536..3d89a94af1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml @@ -682,7 +682,6 @@ Återställ - Nuvarande version Röd text kommer inte att synas i den valda versionen. , Grön betyder att den har tillkommit]]> Dokumentet har återgått till en tidigare version Här visas den valda sidversionen i HTML. Om du vill se skillnaden mellan två versioner samtidigt, välj istället "Diff". diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml index f6e52c2f2c..1195378516 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml @@ -1319,8 +1319,6 @@ Web sitenizi yönetmek için, Umbraco'nun arka ofisini açın ve içerik eklemey Değişiklikler - Oluşturuldu - Mevcut sürüm Kırmızı metin seçili sürümde gösterilmeyecektir. , yeşil eklendi demektir ]]> Belge geri alındı ​​ Mevcut sürümle karşılaştırmak için bir sürüm seçin diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml index 79d09fd4ae..83ea263a97 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml @@ -763,7 +763,6 @@ Reset - 当前版本 红色是选中版本中没有的。绿色是新增的]]> 文档已回滚 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml index 4ba2b0f98d..d334178766 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml @@ -753,7 +753,6 @@ 重設 - 當前版本 紅色 文字將不會顯示於所選版本,而綠色表示增加部分。]]> 文檔已回滾 這顯示所選版本的HTML格式,如果您想要比較兩版本的差異,請使用比較檢視 diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs index 2963618e1b..a610b0f575 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs @@ -34,6 +34,9 @@ public class MemberSave : ContentBaseSave [DataMember(Name = "isApproved")] public bool IsApproved { get; set; } + [DataMember(Name = "isTwoFactorEnabled")] + public bool IsTwoFactorEnabled { get; set; } + private T? GetPropertyValue(string alias) { ContentPropertyBasic? prop = Properties.FirstOrDefault(x => x.Alias == alias); diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 65db6181dd..3c20babecc 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; @@ -6,6 +7,7 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.Mapping; @@ -26,8 +28,36 @@ public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper private readonly IMemberService _memberService; private readonly IMemberGroupService _memberGroupService; private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; - private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly ITwoFactorLoginService _twoFactorLoginService; + // PropertyEditorCollection is still injected as when removing it, the number of + // parameters matches with the obsolete ctor and the two ctors become ambiguous + // [ActivatorUtilitiesConstructor] won't solve the problem in this case. + // PropertyEditorCollection can be removed when the obsolete ctor is removed for + // Umbraco 13 + public MemberTabsAndPropertiesMapper( + ICultureDictionary cultureDictionary, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IMemberTypeService memberTypeService, + IMemberService memberService, + IMemberGroupService memberGroupService, + IOptions memberPasswordConfiguration, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + PropertyEditorCollection propertyEditorCollection, + ITwoFactorLoginService twoFactorLoginService) + : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + _twoFactorLoginService = twoFactorLoginService ?? throw new ArgumentNullException(nameof(twoFactorLoginService)); + } + + [Obsolete("Use constructor that also takes an ITwoFactorLoginService. Scheduled for removal in V13")] public MemberTabsAndPropertiesMapper( ICultureDictionary cultureDictionary, IBackOfficeSecurityAccessor backofficeSecurityAccessor, @@ -38,15 +68,18 @@ public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper IOptions memberPasswordConfiguration, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, PropertyEditorCollection propertyEditorCollection) - : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) + : this( + cultureDictionary, + backofficeSecurityAccessor, + localizedTextService, + memberTypeService, + memberService, + memberGroupService, + memberPasswordConfiguration, + contentTypeBaseServiceProvider, + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); - _memberPasswordConfiguration = memberPasswordConfiguration.Value; - _propertyEditorCollection = propertyEditorCollection; } /// @@ -181,6 +214,8 @@ public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) { + var isTwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key).Result; + var properties = new List { GetLoginProperty(member, _localizedTextService), @@ -246,6 +281,17 @@ public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) }, + // Toggle for disabling Two-Factor Authentication for a Member + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}twoFactorEnabled", + Label = _localizedTextService.Localize("member", "2fa"), + Value = isTwoFactorEnabled, + View = "boolean", + IsSensitive = true, + Readonly = !isTwoFactorEnabled, // The value can't be set to true, so make it readonly when that's the case (you can only disable) + }, + new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index d36baed604..b12cbe58ef 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -23,19 +23,15 @@ public static class PropertyTagsExtensions IDataEditor? editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; TagsPropertyEditorAttribute? tagAttribute = editor?.GetTagAttribute(); - if (tagAttribute == null) - { - return null; - } var configurationObject = property.PropertyType is null ? null : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; - TagConfiguration? configuration = ConfigurationEditor.ConfigurationAs(configurationObject); + TagConfiguration? configuration = configurationObject as TagConfiguration; if (configuration is not null && configuration.Delimiter == default) { - configuration.Delimiter = tagAttribute.Delimiter; + configuration.Delimiter = tagAttribute?.Delimiter ?? ','; } return configuration; diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index 1dec9946d3..1184f2524f 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -16,6 +16,13 @@ public class BlockListConfiguration [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] public NumberRange ValidationLimit { get; set; } = new(); + [ConfigurationField("useSingleBlockMode", "Single block mode", "boolean", + Description = @"When in Single block mode, the output will be BlockListItem<>, instead of BlockListModel. + +**NOTE:** +Single block mode requires a maximum of one available block, and an amount set to minimum 1 and maximum 1 blocks.")] + public bool UseSingleBlockMode { get; set; } + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] public bool UseLiveEditing { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs b/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs new file mode 100644 index 0000000000..f809e787bc --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Resolve tags from values +/// +public interface IDataValueTags +{ + /// + /// Returns any tags contained in the value + /// + /// + /// + /// + /// + IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId); +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index ff92c2012f..8206ab538b 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -5,6 +5,7 @@ namespace Umbraco.Extensions; /// /// Provides extension methods for the interface to manage tags. /// +[Obsolete] public static class PropertyEditorTagsExtensions { /// diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 849d6446a9..d6f1584e8d 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// Marks property editors that support tags. /// [AttributeUsage(AttributeTargets.Class)] +[Obsolete("Implement a custom IDataValueEditor with the IDataValueTags interface instead")] public class TagsPropertyEditorAttribute : Attribute { /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 908c0bf2e0..e236980d1d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs b/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs new file mode 100644 index 0000000000..fec3c07e3c --- /dev/null +++ b/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs @@ -0,0 +1,51 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class GridCellValueConnectorExtensions +{ + /// + /// Gets the value. + /// + /// The connector. + /// The grid control. + /// The dependencies. + /// The context cache. + /// + /// The value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? GetValue(this IGridCellValueConnector connector, GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache) + => connector is IGridCellValueConnector2 connector2 + ? connector2.GetValue(gridControl, dependencies, contextCache) + : connector.GetValue(gridControl, dependencies); + + /// + /// Sets the value. + /// + /// The connector. + /// The grid control. + /// The context cache. + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static void SetValue(this IGridCellValueConnector connector, GridValue.GridControl gridControl, IContextCache contextCache) + { + if (connector is IGridCellValueConnector2 connector2) + { + connector2.SetValue(gridControl, contextCache); + } + else + { + connector.SetValue(gridControl); + } + } +} diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs index a9ed5fd84d..4395202a56 100644 --- a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using static Umbraco.Cms.Core.Models.GridValue; namespace Umbraco.Cms.Core.Deploy; @@ -37,20 +36,8 @@ public interface IGridCellValueConnector /// /// Note that /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies) - => GetValue(gridControl, dependencies, PassThroughCache.Instance); - - /// - /// Gets the value to be deployed from the control value as a string. - /// - /// The control containing the value. - /// The dependencies of the property. - /// The context cache. - /// - /// The grid cell value to be deployed. - /// - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IGridCellValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); /// /// Allows you to modify the value of a control being deployed. @@ -60,18 +47,6 @@ public interface IGridCellValueConnector /// Follows the pattern of the property value connectors (). /// The SetValue method is used to modify the value of the . /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - void SetValue(GridValue.GridControl gridControl) - => SetValue(gridControl, PassThroughCache.Instance); - - /// - /// Allows you to modify the value of a control being deployed. - /// - /// The control being deployed. - /// The context cache. - /// - /// Follows the pattern of the property value connectors (). - /// The SetValue method is used to modify the value of the . - /// - void SetValue(GridValue.GridControl gridControl, IContextCache contextCache); + [Obsolete($"Implement {nameof(IGridCellValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + void SetValue(GridValue.GridControl gridControl); } diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs new file mode 100644 index 0000000000..44a36677e9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IGridCellValueConnector2 : IGridCellValueConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IGridCellValueConnector.GetValue(GridValue.GridControl gridControl, ICollection dependencies) + => GetValue(gridControl, dependencies, PassThroughCache.Instance); + + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The dependencies of the property. + /// The context cache. + /// + /// The grid cell value to be deployed. + /// + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + void IGridCellValueConnector.SetValue(GridValue.GridControl gridControl) + => SetValue(gridControl, PassThroughCache.Instance); + + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// The context cache. + /// + /// Follows the pattern of the property value connectors (). + /// The SetValue method is used to modify the value of the . + /// + void SetValue(GridValue.GridControl gridControl, IContextCache contextCache); +} diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index d8c6d1ff8f..59e4eeb105 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -7,6 +7,7 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Compact; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; @@ -21,7 +22,7 @@ namespace Umbraco.Extensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [Obsolete("Please use an alternative method.")] + [Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")] public static LoggerConfiguration MinimalConfiguration( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -36,7 +37,7 @@ namespace Umbraco.Extensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [Obsolete("Please use an alternative method.")] + [Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")] public static LoggerConfiguration MinimalConfiguration( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -126,6 +127,7 @@ namespace Umbraco.Extensions /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept + [Obsolete("Will be removed in Umbraco 13.")] public static LoggerConfiguration OutputDefaultTextFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -144,6 +146,31 @@ namespace Umbraco.Extensions return logConfig; } + /// + /// Outputs a .txt format log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// + /// The log level you wish the JSON file to collect - default is Verbose (highest) + public static LoggerConfiguration OutputDefaultTextFile( + this LoggerConfiguration logConfig, + IHostEnvironment hostEnvironment, + LoggingSettings loggingSettings, + LogEventLevel minimumLevel = LogEventLevel.Verbose) + { + //Main .txt logfile - in similar format to older Log4Net output + //Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File( + Path.Combine(loggingSettings.GetAbsoluteLoggingPath(hostEnvironment), $"UmbracoTraceLog.{Environment.MachineName}..txt"), + shared: true, + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: minimumLevel, + retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); + + return logConfig; + } /// /// Used in config - If renamed or moved to other assembly the config file also has be updated. @@ -195,6 +222,7 @@ namespace Umbraco.Extensions /// The log level you wish the JSON file to collect - default is Verbose (highest) /// /// The number of days to keep log files. Default is set to null which means all logs are kept + [Obsolete("Will be removed in Umbraco 13.")] public static LoggerConfiguration OutputDefaultJsonFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -215,5 +243,33 @@ namespace Umbraco.Extensions return logConfig; } + /// + /// Outputs a CLEF format JSON log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// The logging configuration + /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// The number of days to keep log files. Default is set to null which means all logs are kept + public static LoggerConfiguration OutputDefaultJsonFile( + this LoggerConfiguration logConfig, + IHostEnvironment hostEnvironment, + LoggingSettings loggingSettings, + LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) + { + // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) + // Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File( + new CompactJsonFormatter(), + Path.Combine(loggingSettings.GetAbsoluteLoggingPath(hostEnvironment) ,$"UmbracoTraceLog.{Environment.MachineName}..json"), + shared: true, + rollingInterval: RollingInterval.Day, // Create a new JSON file every day + retainedFileCountLimit: retainedFileCount, // Setting to null means we keep all files - default is 31 days + restrictedToMinimumLevel: minimumLevel); + + return logConfig; + } + } } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 09cfcf5aaf..8ebd803aac 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -1,7 +1,10 @@ using System.Collections; using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Mapping; @@ -44,19 +47,32 @@ public class UmbracoMapper : IUmbracoMapper private readonly ConcurrentDictionary>> _ctors = new(); - private readonly ConcurrentDictionary>> _maps = + private readonly ConcurrentDictionary>> _maps = new(); private readonly ICoreScopeProvider _scopeProvider; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// /// - public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) + [Obsolete("Please use ctor that takes an ILogger")] + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) : this(profiles, scopeProvider, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The MapDefinitionCollection + /// The scope provider + /// The logger + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider, ILogger logger) { _scopeProvider = scopeProvider; + _logger = logger; foreach (IMapDefinition profile in profiles) { @@ -119,15 +135,15 @@ public class UmbracoMapper : IUmbracoMapper sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; } - Dictionary> sourceMaps = DefineMaps(sourceType); + ConcurrentDictionary> sourceMaps = DefineMaps(sourceType); sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); } private Dictionary> DefineCtors(Type sourceType) => _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); - private Dictionary> DefineMaps(Type sourceType) => - _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + private ConcurrentDictionary> DefineMaps(Type sourceType) => + _maps.GetOrAdd(sourceType, _ => new ConcurrentDictionary>()); #endregion @@ -428,7 +444,7 @@ public class UmbracoMapper : IUmbracoMapper return null; } - if (_maps.TryGetValue(sourceType, out Dictionary>? sourceMap) && + if (_maps.TryGetValue(sourceType, out ConcurrentDictionary>? sourceMap) && sourceMap.TryGetValue(targetType, out Action? map)) { return map; @@ -436,7 +452,7 @@ public class UmbracoMapper : IUmbracoMapper // we *may* run this more than once but it does not matter map = null; - foreach ((Type stype, Dictionary> smap) in _maps) + foreach ((Type stype, ConcurrentDictionary> smap) in _maps) { if (!stype.IsAssignableFrom(sourceType)) { @@ -462,9 +478,9 @@ public class UmbracoMapper : IUmbracoMapper { foreach (KeyValuePair> m in sourceMap) { - if (!_maps[sourceType].TryGetValue(m.Key, out _)) + if (!_maps[sourceType].TryAdd(m.Key, m.Value)) { - _maps[sourceType].Add(m.Key, m.Value); + _logger.LogDebug("Duplicate key was found, don't add to dictionary"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index d8e132440c..bd3fc6849d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -373,7 +373,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { Message = "The database configuration failed with the following message: " + ex.Message + - $"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')", + $"\n Please check log file for additional information (can be found in '{nameof(LoggingSettings)}.{nameof(LoggingSettings.Directory)}')", Success = false, Percentage = "90" }; diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 1b01a3ba47..cc2e20fa02 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -1745,13 +1747,62 @@ internal class DatabaseDataCreator } } - private void CreateLanguageData() => - ConditionalInsert( - Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - "en-us", - new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, - Constants.DatabaseSchema.Tables.Language, - "id"); + private void CreateLanguageData() + { + // For languages we support the installation of records that are additional to the default installed data. + // We can do this as they are specified by ISO code, which is enough to fully detail them. + // All other customizable install data is specified by GUID, and hence we only know about the set that are installed by default. + InstallDefaultDataSettings? languageInstallDefaultDataSettings = _installDefaultDataSettings.Get(Constants.Configuration.NamedOptions.InstallDefaultData.Languages); + if (languageInstallDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values) + { + // Insert the specified languages, ensuring the first is marked as default. + bool isDefault = true; + foreach (var isoCode in languageInstallDefaultDataSettings.Values) + { + if (!TryCreateCulture(isoCode, out CultureInfo? culture)) + { + continue; + } + + var dto = new LanguageDto + { + IsoCode = culture.Name, + CultureName = culture.EnglishName, + IsDefault = isDefault, + }; + _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", true, dto); + isDefault = false; + } + } + else + { + // Conditionally insert the default language. + if (TryCreateCulture("en-US", out CultureInfo? culture)) + { + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + culture.Name, + new LanguageDto { Id = 1, IsoCode = culture.Name, CultureName = culture.EnglishName, IsDefault = true }, + Constants.DatabaseSchema.Tables.Language, + "id"); + } + } + } + + private bool TryCreateCulture(string isoCode, [NotNullWhen(true)] out CultureInfo? culture) + { + try + { + culture = CultureInfo.GetCultureInfo(isoCode); + return true; + } + catch (CultureNotFoundException ex) + { + _logger.LogWarning(ex, "CultureInfo could not be created because culture '{IsoCode}' is not available. The language will not be created.", isoCode); + culture = null; + return false; + } + } private void CreateContentChildTypeData() { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 4632dc568a..ce14f1ec1e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -274,29 +274,59 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { foreach (IProperty property in entity.Properties) { - TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); - if (tagConfiguration == null) + if (PropertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out var editor) is false) { - continue; // not a tags property + continue; } + if (editor.GetValueEditor() is not IDataValueTags tagsProvider) + { + // support for legacy tag editors, everything from here down to the last continue can be removed when TagsPropertyEditorAttribute is removed + TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); + if (tagConfiguration == null) + { + continue; + } + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (IPropertyValue pvalue in property.Values) + { + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + IEnumerable cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + tags.AddRange(cultureTags); + } + + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings + IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + + continue; // not implementing IDataValueTags, continue + } + + object? configuration = DataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + if (property.PropertyType.VariesByCulture()) { var tags = new List(); foreach (IPropertyValue pvalue in property.Values) { - IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); - IEnumerable cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); - tags.AddRange(cultureTags); + tags.AddRange(tagsProvider.GetTags(pvalue.EditedValue, configuration, languageId)); } tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } else { - IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings - IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + IEnumerable tags = tagsProvider.GetTags(property.GetValue(), configuration, null); tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 11b3810640..fbf2239828 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -13,7 +13,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference +internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags { private BlockEditorValues? _blockEditorValues; private readonly IDataTypeService _dataTypeService; @@ -77,6 +77,40 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV return result; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) + { + return Enumerable.Empty(); + } + + var result = new List(); + // loop through all content and settings data + foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (valueEditor is not IDataValueTags tagsProvider) + { + continue; + } + + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + + result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + } + } + + return result; + } + #region Convert database // editor // note: there is NO variant support here diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index d54ce83451..aabe11592a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -69,7 +69,7 @@ public class NestedContentPropertyEditor : DataEditor protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags { private readonly IDataTypeService _dataTypeService; private readonly ILogger _logger; @@ -150,6 +150,36 @@ public class NestedContentPropertyEditor : DataEditor return result; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + IReadOnlyList rows = + _nestedContentValues.GetPropertyValues(value); + + var result = new List(); + + foreach (NestedContentValues.NestedContentRowValue row in rows.ToList()) + { + foreach (KeyValuePair prop in row.PropertyValues + .ToList()) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (valueEditor is not IDataValueTags tagsProvider) + { + continue; + } + + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + + result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + } + } + + return result; + } + #region DB to String public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 1fd6a2db2b..0a6a40a539 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; @@ -68,18 +69,67 @@ public class TagsPropertyEditor : DataEditor protected override IConfigurationEditor CreateConfigurationEditor() => new TagConfigurationEditor(_validators, _ioHelper, _localizedTextService, _editorConfigurationParser); - internal class TagPropertyValueEditor : DataValueEditor + internal class TagPropertyValueEditor : DataValueEditor, IDataValueTags { + private readonly IDataTypeService _dataTypeService; + public TagPropertyValueEditor( ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataEditorAttribute attribute) + DataEditorAttribute attribute, + IDataTypeService dataTypeService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { + _dataTypeService = dataTypeService; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + var strValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(strValue)) return Enumerable.Empty(); + + var tagConfiguration = ConfigurationEditor.ConfigurationAs(dataTypeConfiguration) ?? new TagConfiguration(); + + if (tagConfiguration.Delimiter == default) + tagConfiguration.Delimiter = ','; + + IEnumerable tags; + + switch (tagConfiguration.StorageType) + { + case TagsStorageType.Csv: + tags = strValue.Split(new[] { tagConfiguration.Delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + break; + + case TagsStorageType.Json: + try + { + tags = JsonConvert.DeserializeObject(strValue)?.Select(x => x.Trim()) ?? Enumerable.Empty(); + } + catch (JsonException) + { + //cannot parse, malformed + tags = Enumerable.Empty(); + } + + break; + + default: + throw new NotSupportedException($"Value \"{tagConfiguration.StorageType}\" is not a valid TagsStorageType."); + } + + return tags.Select(x => new Tag + { + Group = tagConfiguration.Group, + Text = x, + LanguageId = languageId, + }); + } + + /// public override IValueRequiredValidator RequiredValidator => new RequiredJsonValueValidator(); @@ -93,14 +143,33 @@ public class TagsPropertyEditor : DataEditor return null; } + var tagConfiguration = editorValue.DataTypeConfiguration as TagConfiguration ?? new TagConfiguration(); + if (tagConfiguration.Delimiter == default) + tagConfiguration.Delimiter = ','; + + string[] trimmedTags = Array.Empty(); + if (editorValue.Value is JArray json) { - return json.HasValues ? json.Select(x => x.Value()) : null; + trimmedTags = json.HasValues ? json.Select(x => x.Value()).OfType().ToArray() : Array.Empty(); + } + else if (string.IsNullOrWhiteSpace(value) == false) + { + trimmedTags = value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); } - if (string.IsNullOrWhiteSpace(value) == false) + if (trimmedTags.Length == 0) { - return value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return null; + } + + switch (tagConfiguration.StorageType) + { + case TagsStorageType.Csv: + return string.Join(tagConfiguration.Delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(); + + case TagsStorageType.Json: + return trimmedTags.Length == 0 ? null : JsonConvert.SerializeObject(trimmedTags); } return null; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index af1c51c37e..b56f43e0ef 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -1,9 +1,14 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using static Umbraco.Cms.Core.PropertyEditors.BlockListConfiguration; @@ -12,18 +17,70 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; [DefaultPropertyValueConverter(typeof(JsonValueConverter))] public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase { + private readonly IContentTypeService _contentTypeService; + private readonly BlockEditorConverter _blockConverter; + private readonly BlockListEditorDataConverter _blockListEditorDataConverter; private readonly IProfilingLogger _proflog; - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) + [Obsolete("Use the constructor with the IContentTypeService")] + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) : this(proflog, blockConverter, StaticServiceProvider.Instance.GetRequiredService()) { } + + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService) : base(blockConverter) { _proflog = proflog; + _blockConverter = blockConverter; + _blockListEditorDataConverter = new BlockListEditorDataConverter(); + _contentTypeService = contentTypeService; } /// public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + var isSingleBlockMode = IsSingleBlockMode(propertyType.DataType); + if (isSingleBlockMode) + { + BlockListConfiguration.BlockConfiguration? block = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration)?.Blocks.FirstOrDefault(); + + ModelType? contentElementType = block?.ContentElementTypeKey is Guid contentElementTypeKey && _contentTypeService.Get(contentElementTypeKey) is IContentType contentType ? ModelType.For(contentType.Alias) : null; + ModelType? settingsElementType = block?.SettingsElementTypeKey is Guid settingsElementTypeKey && _contentTypeService.Get(settingsElementTypeKey) is IContentType settingsType ? ModelType.For(settingsType.Alias) : null; + + if (contentElementType is not null) + { + if (settingsElementType is not null) + { + return typeof(BlockListItem<,>).MakeGenericType(contentElementType, settingsElementType); + } + + return typeof(BlockListItem<>).MakeGenericType(contentElementType); + } + + return typeof(BlockListItem); + } + + return typeof(BlockListModel); + } + + private bool IsSingleBlockMode(PublishedDataType dataType) + { + BlockListConfiguration? config = + ConfigurationEditor.ConfigurationAs(dataType.Configuration); + return (config?.UseSingleBlockMode ?? false) && config?.Blocks.Length == 1 && config?.ValidationLimit?.Min == 1 && config?.ValidationLimit?.Max == 1; + } + + /// + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + /// + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source?.ToString(); + /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { @@ -44,7 +101,7 @@ public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase - + @@ -24,7 +24,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index a4230fca4d..46e69122ac 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -8,9 +8,9 @@ - + - + diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index cb2d081415..28a6012901 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -467,7 +467,7 @@ public class CodeFileController : BackOfficeNotificationsController { case Constants.Trees.PartialViews: if (IsDirectory( - _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.PartialViews, virtualPath)))) + _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.PartialViews, virtualPath)), _fileSystems.PartialViewsFileSystem!)) { _fileService.DeletePartialViewFolder(virtualPath); return Ok(); @@ -482,7 +482,7 @@ public class CodeFileController : BackOfficeNotificationsController case Constants.Trees.PartialViewMacros: if (IsDirectory( - _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.MacroPartials, virtualPath)))) + _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.MacroPartials, virtualPath)), _fileSystems.MacroPartialsFileSystem!)) { _fileService.DeletePartialViewMacroFolder(virtualPath); return Ok(); @@ -497,7 +497,7 @@ public class CodeFileController : BackOfficeNotificationsController case Constants.Trees.Scripts: if (IsDirectory( - _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoScriptsPath, virtualPath)))) + _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoScriptsPath, virtualPath)), _fileSystems.ScriptsFileSystem!)) { _fileService.DeleteScriptFolder(virtualPath); return Ok(); @@ -512,7 +512,7 @@ public class CodeFileController : BackOfficeNotificationsController return new UmbracoProblemResult("No Script or folder found with the specified path", HttpStatusCode.NotFound); case Constants.Trees.Stylesheets: if (IsDirectory( - _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoCssPath, virtualPath)))) + _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoCssPath, virtualPath)), _fileSystems.StylesheetsFileSystem!)) { _fileService.DeleteStyleSheetFolder(virtualPath); return Ok(); @@ -827,13 +827,21 @@ public class CodeFileController : BackOfficeNotificationsController return value; } - private bool IsDirectory(string path) + private bool IsDirectory(string path, IFileSystem fileSystem) { - var dirInfo = new DirectoryInfo(path); + // If it's a physical filesystem check with directory info + if (fileSystem.CanAddPhysical) + { + var dirInfo = new DirectoryInfo(path); - // If you turn off indexing in Windows this will have the attribute: - // `FileAttributes.Directory | FileAttributes.NotContentIndexed` - return (dirInfo.Attributes & FileAttributes.Directory) != 0; + // If you turn off indexing in Windows this will have the attribute: + // `FileAttributes.Directory | FileAttributes.NotContentIndexed` + return (dirInfo.Attributes & FileAttributes.Directory) != 0; + } + + // Otherwise check the filesystem abstraction to see if the folder exists + // Since this is used for delete, it presumably exists if we're trying to delete it + return fileSystem.DirectoryExists(path); } // this is an internal class for passing stylesheet data from the client to the controller while editing diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index 795257800a..36a60843fb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -146,7 +146,9 @@ public abstract class ContentControllerBase : BackOfficeNotificationsController // set the value - tags are special TagsPropertyEditorAttribute? tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); - if (tagAttribute != null) + // when TagsPropertyEditorAttribute is removed this whole if can also be removed + // since the call to sovePropertyValue is all that's needed now + if (tagAttribute is not null && valueEditor is not IDataValueTags) { TagConfiguration? tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType?.Configuration); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index b7220a3941..f06ae05f86 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.ContentApps; @@ -25,6 +26,7 @@ using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.ModelBinders; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; @@ -50,6 +52,7 @@ public class MemberController : ContentControllerBase private readonly IPasswordChanger _passwordChanger; private readonly PropertyEditorCollection _propertyEditors; private readonly ICoreScopeProvider _scopeProvider; + private readonly ITwoFactorLoginService _twoFactorLoginService; private readonly IShortStringHelper _shortStringHelper; private readonly IUmbracoMapper _umbracoMapper; @@ -71,6 +74,43 @@ public class MemberController : ContentControllerBase /// The JSON serializer /// The password changer /// The core scope provider + /// The two factor login service + [ActivatorUtilitiesConstructor] + public MemberController( + ICultureDictionary cultureDictionary, + ILoggerFactory loggerFactory, + IShortStringHelper shortStringHelper, + IEventMessagesFactory eventMessages, + ILocalizedTextService localizedTextService, + PropertyEditorCollection propertyEditors, + IUmbracoMapper umbracoMapper, + IMemberService memberService, + IMemberTypeService memberTypeService, + IMemberManager memberManager, + IDataTypeService dataTypeService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IJsonSerializer jsonSerializer, + IPasswordChanger passwordChanger, + ICoreScopeProvider scopeProvider, + ITwoFactorLoginService twoFactorLoginService) + : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) + { + _propertyEditors = propertyEditors; + _umbracoMapper = umbracoMapper; + _memberService = memberService; + _memberTypeService = memberTypeService; + _memberManager = memberManager; + _dataTypeService = dataTypeService; + _localizedTextService = localizedTextService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _jsonSerializer = jsonSerializer; + _shortStringHelper = shortStringHelper; + _passwordChanger = passwordChanger; + _scopeProvider = scopeProvider; + _twoFactorLoginService = twoFactorLoginService; + } + + [Obsolete("Use constructor that also takes an ITwoFactorLoginService. Scheduled for removal in V13")] public MemberController( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, @@ -87,20 +127,24 @@ public class MemberController : ContentControllerBase IJsonSerializer jsonSerializer, IPasswordChanger passwordChanger, ICoreScopeProvider scopeProvider) - : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) + : this( + cultureDictionary, + loggerFactory, + shortStringHelper, + eventMessages, + localizedTextService, + propertyEditors, + umbracoMapper, + memberService, + memberTypeService, + memberManager, + dataTypeService, + backOfficeSecurityAccessor, + jsonSerializer, + passwordChanger, + scopeProvider, + StaticServiceProvider.Instance.GetRequiredService()) { - _propertyEditors = propertyEditors; - _umbracoMapper = umbracoMapper; - _memberService = memberService; - _memberTypeService = memberTypeService; - _memberManager = memberManager; - _dataTypeService = dataTypeService; - _localizedTextService = localizedTextService; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _jsonSerializer = jsonSerializer; - _shortStringHelper = shortStringHelper; - _passwordChanger = passwordChanger; - _scopeProvider = scopeProvider; } /// @@ -544,6 +588,16 @@ public class MemberController : ContentControllerBase return ValidationProblem("An admin cannot lock a member"); } + // Handle disabling of 2FA + if (!contentItem.IsTwoFactorEnabled) + { + IEnumerable providers = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(contentItem.Key); + foreach (var provider in providers) + { + await _twoFactorLoginService.DisableAsync(contentItem.Key, provider); + } + } + // If we're changing the password... // Handle changing with the member manager & password changer (takes care of other nuances) if (contentItem.Password != null) diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 57082eb0e2..2dc22d54a5 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index c9cd08516d..62ede3ce18 100644 --- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -82,8 +82,9 @@ public static class ServiceCollectionExtensions IHostEnvironment hostEnvironment, IConfiguration configuration) { - // TODO: WEBSITE_RUN_FROM_PACKAGE - can't assume this DIR is writable - we have an IConfiguration instance so a later refactor should be easy enough. - var loggingDir = hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles); + LoggingSettings loggerSettings = GetLoggerSettings(configuration); + + var loggingDir = loggerSettings.GetAbsoluteLoggingPath(hostEnvironment); ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir); var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); @@ -153,6 +154,13 @@ public static class ServiceCollectionExtensions return services; } + private static LoggingSettings GetLoggerSettings(IConfiguration configuration) + { + var loggerSettings = new LoggingSettings(); + configuration.GetSection(Constants.Configuration.ConfigLogging).Bind(loggerSettings); + return loggerSettings; + } + /// /// Called to create the to assign to the /// diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/login.svg b/src/Umbraco.Web.UI.Client/src/assets/img/login.svg new file mode 100644 index 0000000000..3bd280b4af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index bddd95fbbe..02f6164f64 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -323,7 +323,6 @@ $scope.defaultButton = buttons.defaultButton; $scope.subButtons = buttons.subButtons; $scope.page.showPreviewButton = true; - } /** Syncs the content item to it's tree node - this occurs on first load and after saving */ @@ -938,22 +937,25 @@ }; $scope.preview = function (content) { - // Chromes popup blocker will kick in if a window is opened - // without the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?init=true', 'umbpreview'); - // Build the correct path so both /#/ and #/ work. - var query = 'id=' + content.id; - if ($scope.culture) { - query += "#?culture=" + $scope.culture; + const openPreviewWindow = () => { + // Chromes popup blocker will kick in if a window is opened + // without the initial scoped request. This trick will fix that. + // + const previewWindow = $window.open('preview/?init=true', 'umbpreview'); + + // Build the correct path so both /#/ and #/ work. + let query = 'id=' + content.id; + if ($scope.culture) { + query += "#?culture=" + $scope.culture; + } + previewWindow.location.href = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query; } - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query; //The user cannot save if they don't have access to do that, in which case we just want to preview //and that's it otherwise they'll get an unauthorized access message if (!_.contains(content.allowedActions, "A")) { - previewWindow.location.href = redirect; + openPreviewWindow(); } else { var selectedVariant = $scope.content.variants[0]; @@ -967,10 +969,12 @@ } } - //ensure the save flag is set + //reset save flag for all variants + $scope.content.variants.forEach(variant => variant.save = false); + //ensure the save flag is set for the active variant selectedVariant.save = true; performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function (data) { - previewWindow.location.href = redirect; + openPreviewWindow() }, function (err) { //validation issues .... }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 864e07f17c..d461a3b0bc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -206,7 +206,6 @@ * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. */ var notSupportedProperties = [ - "Umbraco.Tags", "Umbraco.UploadField", "Umbraco.ImageCropper", "Umbraco.NestedContent" @@ -654,7 +653,7 @@ blockObject.__scope.$evalAsync(); }); }); - + observer.observe(labelElement[0], {characterData: true, subtree:true}); blockObject.__watchers.push(() => { @@ -671,9 +670,9 @@ $index: this.index + 1, ... this.data }; - + this.__labelScope = Object.assign(this.__labelScope, labelVars); - + $compile(labelElement.contents())(this.__labelScope); }.bind(blockObject) } else { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 4c9ce4f61e..30b456f15b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -299,6 +299,9 @@ case '_umb_lockedOut': saveModel.isLockedOut = prop.value; break; + case '_umb_twoFactorEnabled': + saveModel.isTwoFactorEnabled = prop.value; + break; } }); diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 66fe2e0ae4..74b28dabcb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -20,7 +20,7 @@ background-position: center center; background-repeat: no-repeat; background-size: cover; - background-image: url('../img/login.jpg'); + background-image: url('../img/login.svg'); width: 100%; height: 100%; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index ec30c7e450..b453127613 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -38,12 +38,18 @@ angular.module("umbraco") "disabled": vm.model.clipboardItems.length === 0 }]; - if (vm.model.openClipboard === true) { + if (vm.model.singleBlockMode === true && vm.model.openClipboard === true) { + vm.navigation.splice(0,1); + vm.activeTab = vm.navigation[0]; + } + else if (vm.model.openClipboard === true) { vm.activeTab = vm.navigation[1]; } else { vm.activeTab = vm.navigation[0]; } + + vm.activeTab.active = true; } ); @@ -55,10 +61,16 @@ angular.module("umbraco") }; vm.clickClearClipboard = function () { - vm.onNavigationChanged(vm.navigation[0]); - vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. vm.model.clickClearClipboard(); + if (vm.model.singleBlockMode !== true && vm.model.openClipboard !== true) + { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. + } + else { + vm.close(); + } }; vm.model = $scope.model; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index d270a7171e..d59bbcefcf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -73,11 +73,13 @@ getVersions(); } + function canRollback(version) { + return !version.currentDraftVersion; + } + function changeVersion(version) { - const canRollback = !version.currentDraftVersion && !version.currentPublishedVersion; - - if (canRollback === false) { + if (canRollback(version) === false) { return; } @@ -92,6 +94,7 @@ vm.loadingDiff = true; const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; + vm.previousVersion = null; contentResource.getRollbackVersion(version.versionId, culture) .then(function(data) { vm.previousVersion = data; @@ -99,8 +102,11 @@ vm.previousVersion.displayValue = version.displayValue + ' - ' + version.username; createDiff(vm.currentVersion, vm.previousVersion); + const changed = (part) => part.added || part.removed; + vm.diffHasChanges = vm.diff.name.some(changed) || vm.diff.properties.some((property) => property.diff.some(changed)); + vm.loadingDiff = false; - vm.rollbackButtonDisabled = false; + vm.rollbackButtonDisabled = !vm.diffHasChanges; }, function () { vm.loadingDiff = false; }); @@ -131,11 +137,14 @@ // get current backoffice user and format dates userService.getCurrentUser().then(function (currentUser) { - vm.previousVersions = data.items.map(version => { - var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); - version.displayValue = timestampFormatted; - return version; - }); + vm.previousVersions = data.items + // we don't ever want to show the draft version in the rollback list + .filter(version => version.currentDraftVersion === false) + .map(version => { + var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); + version.displayValue = timestampFormatted; + return version; + }); }); }); } @@ -173,7 +182,7 @@ // copy existing properties, so it doesn't manipulate existing properties on page oldProperty = Utilities.copy(oldProperty); property = Utilities.copy(property); - + // we have to make properties storing values as object into strings (Grid, nested content, etc.) if (property.value instanceof Object) { property.value = JSON.stringify(property.value, null, 1); @@ -188,14 +197,14 @@ // diff requires a string property.value = property.value ? property.value + '' : ''; oldProperty.value = oldProperty.value ? oldProperty.value + '' : ''; - + const diffProperty = { 'alias': property.alias, 'label': property.label, 'diff': property.isObject ? Diff.diffJson(property.value, oldProperty.value) : Diff.diffWords(property.value, oldProperty.value), 'isObject': property.isObject || oldProperty.isObject }; - + vm.diff.properties.push(diffProperty); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index 2ad16e83f6..e6f2a1c3a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -21,7 +21,7 @@ - @@ -42,17 +42,12 @@
-
- Current version: - {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}}) -
-
-
@@ -61,12 +56,11 @@
{{version.username}}
Current version - Current version
-
- + - - This shows the differences between the current version and the selected version
Red text will be + + This shows the differences between the current (draft) version and the selected version
Red text will be removed in the selected version, green text will be added
+ + There are no differences between the current (draft) version and the selected version +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less index e7953a4fea..50c66fb9f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less @@ -14,12 +14,6 @@ position: relative; } - .current-version { - background: @gray-10; - padding: 15px; - margin-bottom: 12px; - } - .culture-select { margin-bottom: 12px; } @@ -29,4 +23,4 @@ font-size: 13px; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index ac929fcb3a..2edb7a4e96 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -196,7 +196,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", { var filter = Utilities.fromJson($scope.model.filter); $scope.model.filter = function (node){ return _.isMatch(node.metaData, filter);}; - } + } else { //convert to object @@ -314,6 +314,15 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", } } + function getRootEntity(id, name) { + return { + icon: "icon-folder", + alias: null, + id, + name + }; + } + //wires up selection function nodeSelectHandler(args) { args.event.preventDefault(); @@ -351,90 +360,66 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", $scope.model.select(args.node); } else { - select(args.node.name, args.node.id); - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } + // If this is a container, construct a base entity to return immediately + let entity = undefined; + if (args.node.nodeType === "container") { + entity = args.node; + // mimic the server response from the EntityController + entity.id = parseInt(entity.id, 10); + entity.metaData.IsContainer = true; + // end-mimic + } + + // apply the item to the model + select(args.node.name, args.node.id, entity); + + //toggle checked state + args.node.selected = !args.node.selected; + } } } /** Method used for selecting a node */ function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - - var rootNode = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - - if (vm.multiPicker) { - if (entity) { - multiSelectItem(entity); - } - else { - multiSelectItem(rootNode); - } - } - else { - $scope.model.selection.push(rootNode); - $scope.model.submit($scope.model); - } + // we do not need to query the server in case an entity is provided + if (entity) { + applySelect(entity); } + // nor do we need to query if a root entity was selected (id < 0) + else if (id < 0) { + applySelect(entity || getRootEntity(id, text)); + } + // otherwise we have to get it from the server else { + entityResource.getById(id, vm.entityType).then(function (ent) { + applySelect(ent); + }); + } + } - if (vm.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } - else { - //otherwise we have to get it from the server - entityResource.getById(id, vm.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - else { - - hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.model.selection.push(entity); - $scope.model.submit($scope.model); - } - else { - //otherwise we have to get it from the server - entityResource.getById(id, vm.entityType).then(function (ent) { - $scope.model.selection.push(ent); - $scope.model.submit($scope.model); - }); - } - } + /** Method used to apply the selected entity to selections and submit the form if need-be **/ + function applySelect(entity) { + multiSelectItem(entity); + if (!vm.multiPicker) { + $scope.model.submit($scope.model); } } function multiSelectItem(item) { - var found = false; - var foundIndex = 0; - - if ($scope.model.selection.length > 0) { - for (var i = 0; $scope.model.selection.length > i; i++) { - var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === parseInt(item.id)) { - found = true; - foundIndex = i; - } - } + if (!vm.multiPicker) { + $scope.model.selection.length = 0; + hideSearch(); } - if (found) { + var foundIndex = -1; + + if ($scope.model.selection.length) { + foundIndex = $scope.model.selection.findIndex(ent => ent.id === parseInt(item.id, 10)); + } + + if (foundIndex !== -1) { $scope.model.selection.splice(foundIndex, 1); } else { @@ -592,7 +577,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", isSearchResult: true }, hasChildren: false, - parent: () => parent + parent: () => parent }); } }); @@ -620,12 +605,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", performFiltering(results); //now actually remove all filtered items so they are not even displayed - results = results.filter(item => !item.filtered); + results = results.filter(item => !item.filtered); vm.searchInfo.results = results; //sync with the curr selected results vm.searchInfo.results.forEach(result => { - var exists = $scope.model.selection.find(item => result.id === item.id); + var exists = $scope.model.selection.find(item => result.id === item.id); if (exists) { result.selected = true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js index be8ddba592..2cb075e80d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js @@ -6,10 +6,9 @@ * @description * The controller for the info view of the datatype editor */ -function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) { +function DataTypeInfoController($scope, $routeParams, dataTypeResource, $timeout, editorService) { var vm = this; - var evts = []; var referencesLoaded = false; vm.references = {}; @@ -48,7 +47,7 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe function open(id, event, type) { // targeting a new tab/window? - if (event.ctrlKey || + if (event.ctrlKey || event.shiftKey || event.metaKey || // apple (event.button && event.button === 1) // middle click, >IE9 + everyone else @@ -85,25 +84,7 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe } } - // load data type references when the references tab is activated - evts.push(eventsService.on("app.tabChange", function (event, args) { - $timeout(function () { - if (args.alias === "info") { - loadRelations(); - } - }); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - - - - + loadRelations(); } angular.module("umbraco").controller("Umbraco.Editors.DataType.InfoController", DataTypeInfoController); diff --git a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html index e7aa63eff0..0f13862628 100644 --- a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html +++ b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html @@ -29,7 +29,7 @@ background-repeat: no-repeat; background-size: cover; background-position: 50%; - background-image: url(/umbraco/assets/img/login.jpg); + background-image: url(/umbraco/assets/img/login.svg); margin: 0; padding: 0; font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html index 710dd15e53..bceef66e37 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html @@ -55,19 +55,19 @@ } :host { - --inherited--column-gap: var(--umb-block-grid--column-gap, 10px); - --inherited--row-gap: var(--umb-block-grid--row-gap, 10px); - --inherited--areas-column-gap: var(--umb-block-grid--areas-column-gap, 10px); - --inherited--areas-row-gap: var(--umb-block-grid--areas-row-gap, 10px); + --inherited--areas-column-gap: var(--umb-block-grid--areas-column-gap, 0); + --inherited--areas-row-gap: var(--umb-block-grid--areas-row-gap, 0); } [part='area-container'] { box-sizing: border-box; padding: 10px; - --umb-block-grid--column-gap: var(--inherited--column-gap, 10px); - --umb-block-grid--row-gap: var(--inherited--row-gap, 10px); - --umb-block-grid--areas-column-gap: var(--inherited--areas-column-gap, 10px); - --umb-block-grid--areas-row-gap: var(--inherited--areas-row-gap, 10px); + --umb-block-grid--areas-column-gap: 10px; + --umb-block-grid--areas-row-gap: 10px; + } + [part='area-container'] slot { + --umb-block-grid--areas-column-gap: var(--inherited--areas-column-gap, 0); + --umb-block-grid--areas-row-gap: var(--inherited--areas-row-gap, 0); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less index 693108f05d..4e5275d788 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less @@ -95,18 +95,13 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-grid__block:not(.--acti &::after { display: var(--umb-block-grid--block-ui-display, block); - animation: umb-block-grid__block__border-pulse 400ms ease-in-out alternate infinite; - @keyframes umb-block-grid__block__border-pulse { - 0% { border-color: rgba(@blueDark, 1); } - 100% { border-color: rgba(@blueDark, 0.66); } - } + border-color: @blueDark; } } &.--active { &::after { display: block; border-color: @pinkLight; - animation: none; } > .umb-block-grid__block--context { @@ -223,15 +218,16 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl .umb-block-grid__block--context { position: absolute; - top: -21px; + bottom: 100%; right: -1px; + max-width: 100%; font-size: 12px; z-index: 4; display: var(--umb-block-grid--block-ui-display, flex); justify-content: end; /** prevent interaction with inline-create button just beneath the context-bar: */ - ::after { + &::after { content: ''; position: absolute; top: 100%; @@ -243,34 +239,62 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl .__context-bar { padding: 0 9px; padding-top: 1px; - background-color: #3544B1; + background-color: @blueDark; color: white; border-top: rgba(255, 255, 255, .7) 1px solid; border-left: rgba(255, 255, 255, .7) 1px solid; border-right: rgba(255, 255, 255, .7) 1px solid; border-top-left-radius: 3px; border-top-right-radius: 3px; - display: inline-block; + display: inline-flex; + box-sizing: border-box; + + max-width: 100%; + white-space: nowrap; + + /* Ensure that the bar is visually connected with the border **/ + &::before { + content: ''; + position: absolute; + left: 0; + bottom: -1px; + width: 1px; + border-top: 1px solid @blueDark; + box-sizing: border-box; + background: none; + } + &::after { + content: ''; + position: absolute; + right: 1px; + bottom: -2px; + height: 2px; + width: 2px; + border-top: 1px solid @blueDark; + border-right: 1px solid @blueDark; + box-sizing: border-box; + background: none; + } + .action { color: currentColor; font-size: 12px; + line-height: inherit; + + overflow: hidden; + text-overflow: ellipsis; } .__divider { - display: inline-block; margin: 0 3px; } .__label { - display: inline-flex; align-items: center; padding: 0; font-weight: 700; user-select: none; - - .icon { - font-size: 22px; - margin-right: 5px; - display: inline-block; - } + + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html index 5189a47df9..d042fbd6e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html @@ -132,6 +132,7 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html index d8621f7a5b..567d20ff25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html @@ -76,18 +76,10 @@ title="blockEditor_focusParentBlock" ng-click="vm.parentBlock.showBlockUI()" ng-class="{ '--error': false && vm.valFormManager.isShowingValidation() }" - > - - {{vm.parentBlock.label}} - - - Focus parent block - - + ng-bind="vm.parentBlock.label" + >
/
-
- {{vm.layoutEntry.$block.label}} -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js index a7be634d0c..acf4dd16ad 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js @@ -72,7 +72,6 @@ const unsubscribe = []; const vm = this; - vm.invalidAmount = false; vm.areaConfig = null; vm.locallyAvailableBlockTypes = 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 30151accc9..a13826b86b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -13,6 +13,7 @@ ng-click="vm.requestShowCreate($index, $event)" ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl" ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)" + ng-if="!vm.singleBlockMode" ng-show="!vm.readonly">
@@ -28,7 +29,7 @@
-
+
-