diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 708f9b98c2..eca2501a63 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -22,6 +22,9 @@ public class SecuritySettings internal const string StaticAllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; + internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60; + internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60; + /// /// Gets or sets a value indicating whether to keep the user logged in. /// @@ -86,6 +89,18 @@ public class SecuritySettings [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; + /// + /// Gets or sets a value for how long (in minutes) a member is locked out when a lockout occurs. + /// + [DefaultValue(StaticMemberDefaultLockoutTimeInMinutes)] + public int MemberDefaultLockoutTimeInMinutes { get; set; } = StaticMemberDefaultLockoutTimeInMinutes; + + /// + /// Gets or sets a value for how long (in minutes) a user is locked out when a lockout occurs. + /// + [DefaultValue(StaticUserDefaultLockoutTimeInMinutes)] + public int UserDefaultLockoutTimeInMinutes { get; set; } = StaticUserDefaultLockoutTimeInMinutes; + /// /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. /// diff --git a/src/Umbraco.Core/PropertyEditors/IBlockValuePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IBlockValuePropertyIndexValueFactory.cs new file mode 100644 index 0000000000..8556b993f0 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IBlockValuePropertyIndexValueFactory.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IBlockValuePropertyIndexValueFactory : IPropertyIndexValueFactory +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/INestedContentPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/INestedContentPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..eb87a390d2 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/INestedContentPropertyIndexValueFactory.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface INestedContentPropertyIndexValueFactory : IPropertyIndexValueFactory +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/ITagPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/ITagPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..33eabb314c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ITagPropertyIndexValueFactory.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface ITagPropertyIndexValueFactory : IPropertyIndexValueFactory +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs b/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs new file mode 100644 index 0000000000..e639ff7ca8 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs @@ -0,0 +1,84 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Abstract base for property index value factories where the value is json. +/// +/// The type to deserialize the json to. +public abstract class JsonPropertyIndexValueFactoryBase : IPropertyIndexValueFactory +{ + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Constructor for the JsonPropertyIndexValueFactoryBase. + /// + protected JsonPropertyIndexValueFactoryBase(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + /// + public IEnumerable>> GetIndexValues( + IProperty property, + string? culture, + string? segment, + bool published) + { + var result = new List>>(); + + var propertyValue = property.GetValue(culture, segment, published); + + // If there is a value, it's a string and it's detected as json. + if (propertyValue is string rawValue && rawValue.DetectIsJson()) + { + try + { + TSerialized? deserializedPropertyValue = _jsonSerializer.Deserialize(rawValue); + + if (deserializedPropertyValue is null) + { + return result; + } + + result.AddRange(Handle(deserializedPropertyValue, property, culture, segment, published)); + } + catch (InvalidCastException) + { + // Swallow...on purpose, there's a chance that this isn't the json format we are looking for + // and we don't want that to affect the website. + } + catch (ArgumentException) + { + // Swallow on purpose to prevent this error: + // Can not add Newtonsoft.Json.Linq.JValue to Newtonsoft.Json.Linq.JObject. + } + } + + result.AddRange(HandleResume(result, property, culture, segment, published)); + + return result; + } + + /// + /// Method to return a list of resume of the content. By default this returns an empty list + /// + protected virtual IEnumerable>> HandleResume( + List>> result, + IProperty property, + string? culture, + string? segment, + bool published) => Array.Empty>>(); + + /// + /// Method that handle the deserialized object. + /// + protected abstract IEnumerable>> Handle( + TSerialized deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published); +} diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..7e64b368c4 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Property Index Valye Factory that do not index anything. +/// +public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory +{ + /// + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) => Array.Empty>>(); +} diff --git a/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..83a327e0ef --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.PropertyEditors; + +public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase, ITagPropertyIndexValueFactory +{ + public TagPropertyIndexValueFactory(IJsonSerializer jsonSerializer) : base(jsonSerializer) + { + } + + protected override IEnumerable>> Handle( + string[] deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published) + { + yield return new KeyValuePair>(property.Alias, deserializedPropertyValue); + } +} diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 822ba89079..ff857986b0 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -76,7 +76,8 @@ public class NotificationService : INotificationService Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody) { - var entitiesL = entities.ToList(); + // sort the entities explicitly by path to handle notification inheritance (see comment below) + var entitiesL = entities.OrderBy(entity => entity.Path).ToList(); // exit if there are no entities if (entitiesL.Count == 0) @@ -84,11 +85,11 @@ public class NotificationService : INotificationService return; } - // put all entity's paths into a list with the same indices - var paths = entitiesL.Select(x => - x.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture)) - .ToArray()) - .ToArray(); + // create a dictionary of entity paths by entity ID + var pathsByEntityId = entitiesL.ToDictionary( + entity => entity.Id, + entity => entity.Path.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray()); // lazily get versions var prevVersionDictionary = new Dictionary(); @@ -106,47 +107,32 @@ public class NotificationService : INotificationService break; } - var i = 0; foreach (IUser user in users) { - // continue if there's no notification for this user - if (notifications[i].UserId != user.Id) + Notification[] userNotifications = notifications.Where(n => n.UserId == user.Id).ToArray(); + foreach (Notification notification in userNotifications) { - continue; // next user - } + // notifications are inherited down the tree - find the topmost entity + // relevant to this notification (entity list is sorted by path) + IContent? entityForNotification = entitiesL + .FirstOrDefault(entity => + pathsByEntityId.TryGetValue(entity.Id, out var path) && + path.Contains(notification.EntityId)); - for (var j = 0; j < entitiesL.Count; j++) - { - IContent content = entitiesL[j]; - var path = paths[j]; - - // test if the notification applies to the path ie to this entity - if (path.Contains(notifications[i].EntityId) == false) + if (entityForNotification == null) { - continue; // next entity + continue; } - if (prevVersionDictionary.ContainsKey(content.Id) == false) + if (prevVersionDictionary.ContainsKey(entityForNotification.Id) == false) { - prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id); + prevVersionDictionary[entityForNotification.Id] = GetPreviousVersion(entityForNotification.Id); } // queue notification - NotificationRequest req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody); + NotificationRequest req = CreateNotificationRequest(operatingUser, user, entityForNotification, prevVersionDictionary[entityForNotification.Id], actionName, siteUri, createSubject, createBody); Enqueue(req); - } - - // skip other notifications for this user, essentially this means moving i to the next index of notifications - // for the next user. - do - { - i++; - } - while (i < notifications.Count && notifications[i].UserId == user.Id); - - if (i >= notifications.Count) - { - break; // break if no more notifications + break; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index a77b00814c..dfe0c34826 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -218,6 +218,19 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddTransient(); + + + builder.AddPropertyIndexValueFactories(); + + return builder; + } + + public static IUmbracoBuilder AddPropertyIndexValueFactories(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index b939a8c06e..acfe6659c5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,6 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.DependencyInjection; + namespace Umbraco.Cms.Core.PropertyEditors; // Scheduled for removal in v12 @@ -11,11 +14,26 @@ public abstract class BlockEditorPropertyEditor : BlockListPropertyEditorBase public const string ContentTypeKeyPropertyKey = "contentTypeKey"; public const string UdiPropertyKey = "udi"; + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] protected BlockEditorPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, PropertyEditorCollection propertyEditors) - : base(dataValueEditorFactory) => + : this( + dataValueEditorFactory, + propertyEditors, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + protected BlockEditorPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) + : base(dataValueEditorFactory, blockValuePropertyIndexValueFactory) + { PropertyEditors = propertyEditors; + } private PropertyEditorCollection PropertyEditors { get; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs index 8881ce82a9..361b8c86f0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs @@ -1,7 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.PropertyEditors; @@ -19,11 +21,25 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase { private readonly IIOHelper _ioHelper; + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] public BlockGridPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper) - : base(dataValueEditorFactory) => + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + + public BlockGridPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) + : base(dataValueEditorFactory, blockValuePropertyIndexValueFactory) + { _ioHelper = ioHelper; + } + #region Pre Value Editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 42a5931a2b..73b767bd4f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -9,6 +10,7 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using BlockGridAreaConfiguration = Umbraco.Cms.Core.PropertyEditors.BlockGridConfiguration.BlockGridAreaConfiguration; @@ -19,9 +21,24 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// public abstract class BlockGridPropertyEditorBase : DataEditor { + private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory; + + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) => + : this(dataValueEditorFactory, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) + : base(dataValueEditorFactory) + { + _blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory; SupportsReadOnly = true; + } + + public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory; + #region Value Editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index b014c7d8c0..438871b8b4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -25,7 +25,7 @@ public class BlockListPropertyEditor : BlockEditorPropertyEditor private readonly IIOHelper _ioHelper; // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] public BlockListPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, PropertyEditorCollection propertyEditors, @@ -34,12 +34,29 @@ public class BlockListPropertyEditor : BlockEditorPropertyEditor { } + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] public BlockListPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, PropertyEditorCollection propertyEditors, IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory, propertyEditors) + : this( + dataValueEditorFactory, + propertyEditors, + ioHelper, + editorConfigurationParser, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser, + IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) + : base(dataValueEditorFactory, propertyEditors, blockValuePropertyIndexValueFactory) { _ioHelper = ioHelper; _editorConfigurationParser = editorConfigurationParser; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index eb09f405a0..194383560e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -6,6 +7,7 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.PropertyEditors; @@ -14,9 +16,25 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// public abstract class BlockListPropertyEditorBase : DataEditor { + + private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory; + + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) => + : this(dataValueEditorFactory, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) + : base(dataValueEditorFactory) + { + _blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory; SupportsReadOnly = true; + } + + public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory; + #region Value Editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs new file mode 100644 index 0000000000..dfedeedc3f --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs @@ -0,0 +1,35 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal sealed class BlockValuePropertyIndexValueFactory : + NestedPropertyIndexValueFactoryBase, + IBlockValuePropertyIndexValueFactory +{ + private readonly IContentTypeService _contentTypeService; + + + public BlockValuePropertyIndexValueFactory( + PropertyEditorCollection propertyEditorCollection, + IContentTypeService contentTypeService, + IJsonSerializer jsonSerializer) + : base(propertyEditorCollection, jsonSerializer) + { + _contentTypeService = contentTypeService; + } + + + protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input) => + _contentTypeService.Get(input.ContentTypeKey); + + protected override IDictionary GetRawProperty(BlockItemData blockItemData) => + blockItemData.RawPropertyValues; + + protected override IEnumerable GetDataItems(BlockValue input) => input.ContentData; +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs index 1ef03bb5ab..56f43c0762 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs @@ -45,6 +45,9 @@ public class ColorPickerPropertyEditor : DataEditor SupportsReadOnly = true; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory(); + + /// protected override IConfigurationEditor CreateConfigurationEditor() => new ColorPickerConfigurationEditor(_ioHelper, _jsonSerializer, _editorConfigurationParser); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index e21d040dbd..0be7cd4f72 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -97,6 +97,8 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, SupportsReadOnly = true; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory(); + public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) { if (propertyEditorAlias == Alias && diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index f6198135c0..c2dedef852 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -58,6 +58,8 @@ public class MediaPicker3PropertyEditor : DataEditor SupportsReadOnly = true; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory(); + /// protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(_ioHelper, _editorConfigurationParser); @@ -66,6 +68,8 @@ public class MediaPicker3PropertyEditor : DataEditor protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + + internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference { private readonly IDataTypeService _dataTypeService; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index aabe11592a..e30c400254 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -33,9 +33,9 @@ public class NestedContentPropertyEditor : DataEditor public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; private readonly IEditorConfigurationParser _editorConfigurationParser; private readonly IIOHelper _ioHelper; + private readonly INestedContentPropertyIndexValueFactory _nestedContentPropertyIndexValueFactory; - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 12.")] public NestedContentPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper) @@ -43,17 +43,35 @@ public class NestedContentPropertyEditor : DataEditor { } + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] public NestedContentPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : this( + dataValueEditorFactory, + ioHelper, + editorConfigurationParser, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public NestedContentPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser, + INestedContentPropertyIndexValueFactory nestedContentPropertyIndexValueFactory) : base(dataValueEditorFactory) { _ioHelper = ioHelper; _editorConfigurationParser = editorConfigurationParser; + _nestedContentPropertyIndexValueFactory = nestedContentPropertyIndexValueFactory; SupportsReadOnly = true; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory => _nestedContentPropertyIndexValueFactory; + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..445e1cc361 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyIndexValueFactory.cs @@ -0,0 +1,37 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal sealed class NestedContentPropertyIndexValueFactory + : NestedPropertyIndexValueFactoryBase< + NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue[], + NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue>, + INestedContentPropertyIndexValueFactory +{ + private readonly IContentTypeService _contentTypeService; + + + public NestedContentPropertyIndexValueFactory( + PropertyEditorCollection propertyEditorCollection, + IContentTypeService contentTypeService, + IJsonSerializer jsonSerializer) : base(propertyEditorCollection, jsonSerializer) + { + _contentTypeService = contentTypeService; + } + + protected override IContentType? GetContentTypeOfNestedItem( + NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue input) + => _contentTypeService.Get(input.ContentTypeAlias); + + protected override IDictionary GetRawProperty( + NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue nestedContentRowValue) => + nestedContentRowValue.RawPropertyValues; + + protected override IEnumerable GetDataItems( + NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue[] input) => input; +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs new file mode 100644 index 0000000000..cc2f8143b8 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs @@ -0,0 +1,181 @@ +using System.Text; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal abstract class NestedPropertyIndexValueFactoryBase : JsonPropertyIndexValueFactoryBase +{ + private readonly PropertyEditorCollection _propertyEditorCollection; + + protected NestedPropertyIndexValueFactoryBase( + PropertyEditorCollection propertyEditorCollection, + IJsonSerializer jsonSerializer) + : base(jsonSerializer) + { + _propertyEditorCollection = propertyEditorCollection; + } + + protected override IEnumerable>> Handle( + TSerialized deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published) + { + var result = new List>>(); + + foreach (TItem nestedContentRowValue in GetDataItems(deserializedPropertyValue)) + { + IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue); + + if (contentType is null) + { + continue; + } + + var propertyTypeDictionary = + contentType + .PropertyGroups + .SelectMany(x => x.PropertyTypes!) + .ToDictionary(x => x.Alias); + + result.AddRange(GetNestedResults( + property.Alias, + culture, + segment, + published, + propertyTypeDictionary, + nestedContentRowValue)); + } + + return RenameKeysToEnsureRawSegmentsIsAPrefix(result); + } + + /// + /// Rename keys that count the RAW-constant, to ensure the RAW-constant is a prefix. + /// + private IEnumerable>> RenameKeysToEnsureRawSegmentsIsAPrefix( + List>> indexContent) + { + foreach (KeyValuePair> indexedKeyValuePair in indexContent) + { + // Tests if key includes the RawFieldPrefix and it is not in the start + if (indexedKeyValuePair.Key.Substring(1).Contains(UmbracoExamineFieldNames.RawFieldPrefix)) + { + var newKey = UmbracoExamineFieldNames.RawFieldPrefix + + indexedKeyValuePair.Key.Replace(UmbracoExamineFieldNames.RawFieldPrefix, string.Empty); + yield return new KeyValuePair>(newKey, indexedKeyValuePair.Value); + } + else + { + yield return indexedKeyValuePair; + } + } + } + + /// + /// Gets the content type using the nested item. + /// + protected abstract IContentType? GetContentTypeOfNestedItem(TItem nestedItem); + + /// + /// Gets the raw data from a nested item. + /// + protected abstract IDictionary GetRawProperty(TItem nestedItem); + + /// + /// Get the data times of a parent item. E.g. block list have contentData. + /// + protected abstract IEnumerable GetDataItems(TSerialized input); + + /// + /// Index a key with the name of the property, using the relevant content of all the children. + /// + protected override IEnumerable>> HandleResume( + List>> indexedContent, + IProperty property, + string? culture, + string? segment, + bool published) + { + yield return new KeyValuePair>( + property.Alias, + GetResumeFromAllContent(indexedContent).Yield()); + } + + /// + /// Gets a resume as string of all the content in this nested type. + /// + /// All the indexed content for this property. + /// the string with all relevant content from + private static string GetResumeFromAllContent(List>> indexedContent) + { + var stringBuilder = new StringBuilder(); + foreach ((var indexKey, IEnumerable? indexedValue) in indexedContent) + { + // Ignore Raw fields + if (indexKey.Contains(UmbracoExamineFieldNames.RawFieldPrefix)) + { + continue; + } + + foreach (var value in indexedValue) + { + if (value is not null) + { + stringBuilder.AppendLine(value.ToString()); + } + } + } + + return stringBuilder.ToString(); + } + + /// + /// Gets the content to index for the nested type. E.g. Block list, Nested Content, etc.. + /// + private IEnumerable>> GetNestedResults( + string keyPrefix, + string? culture, + string? segment, + bool published, + IDictionary propertyTypeDictionary, + TItem nestedContentRowValue) + { + var blockIndex = 0; + + foreach ((var propertyAlias, var propertyValue) in GetRawProperty(nestedContentRowValue)) + { + if (propertyTypeDictionary.TryGetValue(propertyAlias, out IPropertyType? propertyType)) + { + IProperty subProperty = new Property(propertyType); + subProperty.SetValue(propertyValue, culture, segment); + + if (published) + { + subProperty.PublishValues(culture, segment ?? "*"); + } + + IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias]; + if (editor is null) + { + continue; + } + + IEnumerable>> indexValues = + editor.PropertyIndexValueFactory.GetIndexValues(subProperty, culture, segment, published); + + foreach ((var nestedAlias, IEnumerable nestedValue) in indexValues) + { + yield return new KeyValuePair>( + $"{keyPrefix}.items[{blockIndex}].{nestedAlias}", nestedValue!); + } + } + + blockIndex++; + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 0a6a40a539..1923950fe0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -29,12 +29,13 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class TagsPropertyEditor : DataEditor { private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly ITagPropertyIndexValueFactory _tagPropertyIndexValueFactory; private readonly IIOHelper _ioHelper; private readonly ILocalizedTextService _localizedTextService; private readonly ManifestValueValidatorCollection _validators; // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] public TagsPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, ManifestValueValidatorCollection validators, @@ -45,24 +46,48 @@ public class TagsPropertyEditor : DataEditor validators, ioHelper, localizedTextService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] + public TagsPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ManifestValueValidatorCollection validators, + IIOHelper ioHelper, + ILocalizedTextService localizedTextService, + IEditorConfigurationParser editorConfigurationParser) + : this( + dataValueEditorFactory, + validators, + ioHelper, + localizedTextService, + editorConfigurationParser, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + public TagsPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService, - IEditorConfigurationParser editorConfigurationParser) + IEditorConfigurationParser editorConfigurationParser, + ITagPropertyIndexValueFactory tagPropertyIndexValueFactory) : base(dataValueEditorFactory) { _validators = validators; _ioHelper = ioHelper; _localizedTextService = localizedTextService; _editorConfigurationParser = editorConfigurationParser; + _tagPropertyIndexValueFactory = tagPropertyIndexValueFactory; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory => _tagPropertyIndexValueFactory; + + protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs index a480991648..e3f897018a 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs @@ -1,8 +1,10 @@ using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Security; @@ -13,9 +15,21 @@ namespace Umbraco.Cms.Web.BackOffice.Security; public sealed class ConfigureBackOfficeIdentityOptions : IConfigureOptions { private readonly UserPasswordConfigurationSettings _userPasswordConfiguration; + private readonly SecuritySettings _securitySettings; - public ConfigureBackOfficeIdentityOptions(IOptions userPasswordConfiguration) => + [Obsolete("Use the constructor that accepts SecuritySettings. Will be removed in V13.")] + public ConfigureBackOfficeIdentityOptions(IOptions userPasswordConfiguration) + : this(userPasswordConfiguration, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public ConfigureBackOfficeIdentityOptions( + IOptions userPasswordConfiguration, + IOptions securitySettings) + { _userPasswordConfiguration = userPasswordConfiguration.Value; + _securitySettings = securitySettings.Value; + } public void Configure(BackOfficeIdentityOptions options) { @@ -31,8 +45,7 @@ public sealed class ConfigureBackOfficeIdentityOptions : IConfigureOptions
-
+
Provider: {{installer.current.model.quickInstallSettings.displayName}}
Name: {{installer.current.model.quickInstallSettings.defaultDatabaseName}}
+
+ A database has been pre-configured for your installation. +
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 947c3beecd..d88a9689ab 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -95,7 +95,8 @@ public class DataValueEditorReuseTests _dataValueEditorFactoryMock.Object, new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); // block list is *not* set to reuse its data value editor var dataValueEditor1 = blockListPropertyEditor.GetValueEditor(); @@ -115,7 +116,8 @@ public class DataValueEditorReuseTests _dataValueEditorFactoryMock.Object, new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); // no matter what, a property editor should never reuse its data value editor when created *with* configuration var dataValueEditor1 = blockListPropertyEditor.GetValueEditor("config"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs index 7109b9cbcc..1fa01615ed 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs @@ -34,7 +34,11 @@ public class NestedContentTests var localizationService = Mock.Of(); PropertyEditorCollection editors = null; - var editor = new NestedContentPropertyEditor(Mock.Of(), Mock.Of(), Mock.Of()); + var editor = new NestedContentPropertyEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); editors = new PropertyEditorCollection(new DataEditorCollection(() => new DataEditor[] { editor })); var serializer = new ConfigurationEditorJsonSerializer(); diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index 32da8f798a..f31dd131e8 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -12,5 +12,8 @@ + + +