diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index 2760181536..7d6b297611 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -19,6 +19,11 @@ + + + + + diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs index 0b761e125d..6ffa5c4a1e 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Options; @@ -49,16 +50,32 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) + if (context.Commands.Contains(ResizeWebProcessor.Width)) { - context.Commands.Remove(ResizeWebProcessor.Width); + if (!int.TryParse( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var width) + || width < 0 + || width >= _imagingSettings.Resize.MaxWidth) + { + context.Commands.Remove(ResizeWebProcessor.Width); + } } - int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) + if (context.Commands.Contains(ResizeWebProcessor.Height)) { - context.Commands.Remove(ResizeWebProcessor.Height); + if (!int.TryParse( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var height) + || height < 0 + || height >= _imagingSettings.Resize.MaxHeight) + { + context.Commands.Remove(ResizeWebProcessor.Height); + } } return Task.CompletedTask; diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Cms.Imaging.ImageSharp2/ConfigureImageSharpMiddlewareOptions.cs index 8daa1b689b..dcc67bf5d3 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/ConfigureImageSharpMiddlewareOptions.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Options; @@ -47,20 +48,32 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions( - context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), - context.Culture); - if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) + if (context.Commands.Contains(ResizeWebProcessor.Width)) { - context.Commands.Remove(ResizeWebProcessor.Width); + if (!int.TryParse( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var width) + || width < 0 + || width >= _imagingSettings.Resize.MaxWidth) + { + context.Commands.Remove(ResizeWebProcessor.Width); + } } - var height = context.Parser.ParseValue( - context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), - context.Culture); - if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) + if (context.Commands.Contains(ResizeWebProcessor.Height)) { - context.Commands.Remove(ResizeWebProcessor.Height); + if (!int.TryParse( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var height) + || height < 0 + || height >= _imagingSettings.Resize.MaxHeight) + { + context.Commands.Remove(ResizeWebProcessor.Height); + } } return Task.CompletedTask; diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index e630dad699..c5f2e66ac0 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -15,6 +15,12 @@ + + + + + + diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 7d2556ea60..036f57074f 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -117,6 +117,7 @@ public class GlobalSettings /// /// Gets or sets a value indicating whether to install the database when it is missing. /// + [Obsolete("This option will be removed in V16.")] [DefaultValue(StaticInstallMissingDatabase)] public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase; diff --git a/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs index 44766fb2dc..c5c28ffcfa 100644 --- a/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs +++ b/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs @@ -27,7 +27,9 @@ public class RootDynamicRootOriginFinder : IDynamicRootOriginFinder return null; } - var entity = _entityService.Get(query.Context.ParentKey); + // when creating new content, CurrentKey will be null - fallback to using ParentKey + Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey; + var entity = _entityService.Get(entityKey); if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false) { diff --git a/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs index d1e515de59..f9b207db03 100644 --- a/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs +++ b/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs @@ -20,12 +20,14 @@ public class SiteDynamicRootOriginFinder : RootDynamicRootOriginFinder public override Guid? FindOriginKey(DynamicRootNodeQuery query) { - if (query.OriginAlias != SupportedOriginType || query.Context.CurrentKey.HasValue is false) + if (query.OriginAlias != SupportedOriginType) { return null; } - IEntitySlim? entity = _entityService.Get(query.Context.CurrentKey.Value); + // when creating new content, CurrentKey will be null - fallback to using ParentKey + Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey; + IEntitySlim? entity = _entityService.Get(entityKey); if (entity is null || entity.NodeObjectType != Constants.ObjectTypes.Document) { return null; diff --git a/src/Umbraco.Core/Routing/UriUtility.cs b/src/Umbraco.Core/Routing/UriUtility.cs index fb59ada249..1869641fb5 100644 --- a/src/Umbraco.Core/Routing/UriUtility.cs +++ b/src/Umbraco.Core/Routing/UriUtility.cs @@ -111,6 +111,12 @@ public sealed class UriUtility if (path != "/") { path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + + // perform fallback to root if the path was all slashes (i.e. https://some.where//////) + if (path == string.Empty) + { + path = "/"; + } } return uri.Rewrite(path); diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index a97f753f99..344a5c8551 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,3 +1,4 @@ +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; @@ -315,6 +316,21 @@ public interface IContentService : IContentServiceBase /// OperationResult Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId); + /// + /// Attempts to move the to under the node with id . + /// + /// The that shall be moved. + /// The id of the new parent node. + /// Id of the user attempting to move . + /// Success if moving succeeded, otherwise Failed. + [Obsolete("Adds return type to Move method. Will be removed in V14, as the original method will be adjusted.")] + OperationResult + AttemptMove(IContent content, int parentId, int userId = Constants.Security.SuperUserId) + { + Move(content, parentId, userId); + return OperationResult.Succeed(new EventMessages()); + } + /// /// Copies a document. /// diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs index 1634f60baa..51012200bd 100644 --- a/src/Umbraco.Core/Services/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/LocalizedTextService.cs @@ -350,7 +350,13 @@ public class LocalizedTextService : ILocalizedTextService IEnumerable areas = xmlSource[cult].Value.XPathSelectElements("//area"); foreach (XElement area in areas) { - var result = new Dictionary(StringComparer.InvariantCulture); + var areaAlias = area.Attribute("alias")!.Value; + + if (!overallResult.TryGetValue(areaAlias, out IDictionary? result)) + { + result = new Dictionary(StringComparer.InvariantCulture); + } + IEnumerable keys = area.XPathSelectElements("./key"); foreach (XElement key in keys) { @@ -364,7 +370,10 @@ public class LocalizedTextService : ILocalizedTextService } } - overallResult.Add(area.Attribute("alias")!.Value, result); + if (!overallResult.ContainsKey(areaAlias)) + { + overallResult.Add(areaAlias, result); + } } // Merge English Dictionary @@ -374,11 +383,11 @@ public class LocalizedTextService : ILocalizedTextService IEnumerable enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area"); foreach (XElement area in enUS) { - IDictionary - result = new Dictionary(StringComparer.InvariantCulture); - if (overallResult.ContainsKey(area.Attribute("alias")!.Value)) + var areaAlias = area.Attribute("alias")!.Value; + + if (!overallResult.TryGetValue(areaAlias, out IDictionary? result)) { - result = overallResult[area.Attribute("alias")!.Value]; + result = new Dictionary(StringComparer.InvariantCulture); } IEnumerable keys = area.XPathSelectElements("./key"); @@ -394,9 +403,9 @@ public class LocalizedTextService : ILocalizedTextService } } - if (!overallResult.ContainsKey(area.Attribute("alias")!.Value)) + if (!overallResult.ContainsKey(areaAlias)) { - overallResult.Add(area.Attribute("alias")!.Value, result); + overallResult.Add(areaAlias, result); } } } diff --git a/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs new file mode 100644 index 0000000000..1dc5f42a01 --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs @@ -0,0 +1,33 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +public class MemberRepositoryUsernameCachePolicy : DefaultRepositoryCachePolicy +{ + public MemberRepositoryUsernameCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) : base(cache, scopeAccessor, options) + { + } + + public IMember? GetByUserName(string key, string? username, Func performGetByUsername, Func?> performGetAll) + { + var cacheKey = GetEntityCacheKey(key + username); + IMember? fromCache = Cache.GetCacheItem(cacheKey); + + // if found in cache then return else fetch and cache + if (fromCache != null) + { + return fromCache; + } + + IMember? entity = performGetByUsername(username); + + if (entity != null && entity.HasIdentity) + { + InsertEntity(cacheKey, entity); + } + + return entity; + } +} diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 8ad2b07b23..9bf5c91eb8 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -1,5 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Logging; @@ -23,19 +27,39 @@ public class UnattendedUpgrader : INotificationAsyncHandler unattendedSettings) { _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); _packageMigrationRunner = packageMigrationRunner; + _unattendedSettings = unattendedSettings.Value; + } + + [Obsolete("Use constructor that takes IOptions, this will be removed in V16")] + public UnattendedUpgrader( + IProfilingLogger profilingLogger, + IUmbracoVersion umbracoVersion, + DatabaseBuilder databaseBuilder, + IRuntimeState runtimeState, + PackageMigrationRunner packageMigrationRunner) + : this( + profilingLogger, + umbracoVersion, + databaseBuilder, + runtimeState, + packageMigrationRunner, + StaticServiceProvider.Instance.GetRequiredService>()) + { } public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) @@ -46,55 +70,26 @@ public class UnattendedUpgrader : INotificationAsyncHandler( - "Starting unattended upgrade.", - "Unattended upgrade completed.")) - { - DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - if (result?.Success == false) - { - var innerException = new UnattendedInstallException( - "An error occurred while running the unattended upgrade.\n" + result.Message); - _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); - } + RunUpgrade(notification); - notification.UnattendedUpgradeResult = - RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; + // If we errored out when upgrading don't do anything. + if (notification.UnattendedUpgradeResult is RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors) + { + return Task.CompletedTask; + } + + // It's entirely possible that there's both a core upgrade and package migrations to run, so try and run package migrations too. + // but only if upgrade unattended is enabled. + if (_unattendedSettings.PackageMigrationsUnattended) + { + RunPackageMigrations(notification); } } break; case RuntimeLevelReason.UpgradePackageMigrations: { - if (!_runtimeState.StartupState.TryGetValue( - RuntimeState.PendingPackageMigrationsStateKey, - out var pm) - || pm is not IReadOnlyList pendingMigrations) - { - throw new InvalidOperationException( - $"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); - } - - if (pendingMigrations.Count == 0) - { - throw new InvalidOperationException( - "No pending migrations found but the runtime level reason is " + - RuntimeLevelReason.UpgradePackageMigrations); - } - - try - { - _packageMigrationRunner.RunPackagePlans(pendingMigrations); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult - .PackageMigrationComplete; - } - catch (Exception ex) - { - SetRuntimeError(ex); - notification.UnattendedUpgradeResult = - RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; - } + RunPackageMigrations(notification); } break; @@ -106,6 +101,64 @@ public class UnattendedUpgrader : INotificationAsyncHandler pendingMigrations) + { + throw new InvalidOperationException( + $"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); + } + + if (pendingMigrations.Count == 0) + { + // If we determined we needed to run package migrations but there are none, this is an error + if (_runtimeState.Reason is RuntimeLevelReason.UpgradePackageMigrations) + { + throw new InvalidOperationException( + "No pending migrations found but the runtime level reason is " + + RuntimeLevelReason.UpgradePackageMigrations); + } + + return; + } + + try + { + _packageMigrationRunner.RunPackagePlans(pendingMigrations); + notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult + .PackageMigrationComplete; + } + catch (Exception ex) + { + SetRuntimeError(ex); + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; + } + } + + private void RunUpgrade(RuntimeUnattendedUpgradeNotification notification) + { + var plan = new UmbracoPlan(_umbracoVersion); + using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Verbose) ? null : _profilingLogger.TraceDuration( + "Starting unattended upgrade.", + "Unattended upgrade completed.")) + { + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + if (result?.Success == false) + { + var innerException = new UnattendedInstallException( + "An error occurred while running the unattended upgrade.\n" + result.Message); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + } + + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; + } + } + private void SetRuntimeError(Exception exception) => _runtimeState.Configure( RuntimeLevel.BootFailed, diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index 9f0df27897..d7dc4f8161 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -104,7 +104,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) @@ -144,12 +144,12 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe protected override IAuditItem? PerformGet(int id) { Sql sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.Where(GetBaseWhereClause(), new { id = id }); LogDto? dto = Database.First(sql); return dto == null ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp); } protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); @@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); } protected override Sql GetBaseQuery(bool isCount) @@ -184,7 +184,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe return sql; } - protected override string GetBaseWhereClause() => "id = @id"; + protected override string GetBaseWhereClause() => "umbracoLog.id = @id"; protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 6be4c36376..1f7c5519d5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; public class MemberRepository : ContentRepositoryBase, IMemberRepository { private readonly IJsonSerializer _jsonSerializer; - private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly MemberRepositoryUsernameCachePolicy _memberByUsernameCachePolicy; private readonly IMemberGroupRepository _memberGroupRepository; private readonly IMemberTypeRepository _memberTypeRepository; private readonly MemberPasswordConfigurationSettings _passwordConfiguration; @@ -68,7 +68,7 @@ public class MemberRepository : ContentRepositoryBase(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + new MemberRepositoryUsernameCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); } /// @@ -326,7 +326,7 @@ public class MemberRepository : ContentRepositoryBase - _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + _memberByUsernameCachePolicy.GetByUserName("uRepo_userNameKey+", username, PerformGetByUsername, PerformGetAllByUsername); public int[] GetMemberIds(string[] usernames) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs index 8321915677..b67e4af0a2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Globalization; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -51,8 +52,8 @@ public class SliderPropertyEditor : DataEditor public override object? ToEditor(IProperty property, string? culture = null, string? segment = null) { - // value is stored as a string - either a single integer value - // or a two integer values separated by comma (for range sliders) + // value is stored as a string - either a single decimal value + // or a two decimal values separated by comma (for range sliders) var value = property.GetValue(culture, segment); if (value is not string stringValue) { @@ -61,7 +62,7 @@ public class SliderPropertyEditor : DataEditor var parts = stringValue.Split(Constants.CharArrays.Comma); var parsed = parts - .Select(s => int.TryParse(s, out var i) ? i : (int?)null) + .Select(s => decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var i) ? i : (decimal?)null) .Where(i => i != null) .Select(i => i!.Value) .ToArray(); @@ -78,11 +79,11 @@ public class SliderPropertyEditor : DataEditor internal class SliderRange { - public int From { get; set; } + public decimal From { get; set; } - public int To { get; set; } + public decimal To { get; set; } - public override string ToString() => From == To ? $"{From}" : $"{From},{To}"; + public override string ToString() => From == To ? $"{From.ToString(CultureInfo.InvariantCulture)}" : $"{From.ToString(CultureInfo.InvariantCulture)},{To.ToString(CultureInfo.InvariantCulture)}"; } } } diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 202810c02b..9e0b0dc781 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -259,18 +259,17 @@ public class RuntimeState : IRuntimeState // All will be prefixed with the same key. IReadOnlyDictionary? keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); - // This could need both an upgrade AND package migrations to execute but - // we will process them one at a time, first the upgrade, then the package migrations. + // This could need both an upgrade AND package migrations to execute, so always add any pending package migrations + IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); + _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; + if (DoesUmbracoRequireUpgrade(keyValues)) { return UmbracoDatabaseState.NeedsUpgrade; } - IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); if (packagesRequiringMigration.Count > 0) { - _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; - return UmbracoDatabaseState.NeedsPackageMigration; } } diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index 0f2da0ac4e..0a84f318f6 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -59,6 +59,14 @@ public static class HttpContextExtensions await httpContext.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType); } + // Update the HttpContext's user with the authenticated user's principal to ensure + // that subsequent requests within the same context will recognize the user + // as authenticated. + if (result.Succeeded) + { + httpContext.User = result.Principal; + } + return result; } diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index 266bf7b2db..df5254141d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -116,6 +116,7 @@ test('can trash a folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.clickConfirmTrashButton(); // Assert + await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.folderDeleted); await umbracoUi.media.isTreeItemVisible(folderName, false); expect(await umbracoApi.media.doesNameExist(folderName)).toBeFalsy(); }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs index 57f484adf2..8e7eed1f28 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs @@ -1,11 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; @@ -24,6 +24,10 @@ public class AuditRepositoryTest : UmbracoIntegrationTest private ILogger _logger; + private IAuditRepository AuditRepository => GetRequiredService(); + + private IAuditItem GetAuditItem(int id) => new AuditItem(id, AuditType.System, -1, UmbracoObjectTypes.Document.GetName(), "This is a System audit trail"); + [Test] public void Can_Add_Audit_Entry() { @@ -40,6 +44,38 @@ public class AuditRepositoryTest : UmbracoIntegrationTest } } + [Test] + public void Has_Create_Date_When_Get_By_Id() + { + using var scope = ScopeProvider.CreateScope(); + + AuditRepository.Save(GetAuditItem(1)); + var auditEntry = AuditRepository.Get(1); + Assert.That(auditEntry.CreateDate, Is.Not.EqualTo(default(DateTime))); + } + + [Test] + public void Has_Create_Date_When_Get_By_Query() + { + using var scope = ScopeProvider.CreateScope(); + + AuditRepository.Save(GetAuditItem(1)); + var auditEntry = AuditRepository.Get(AuditType.System, ScopeProvider.CreateQuery().Where(x => x.Id == 1)).FirstOrDefault(); + Assert.That(auditEntry, Is.Not.Null); + Assert.That(auditEntry.CreateDate, Is.Not.EqualTo(default(DateTime))); + } + + [Test] + public void Has_Create_Date_When_Get_By_Paged_Query() + { + using var scope = ScopeProvider.CreateScope(); + + AuditRepository.Save(GetAuditItem(1)); + var auditEntry = AuditRepository.GetPagedResultsByQuery(ScopeProvider.CreateQuery().Where(x => x.Id == 1),0, 10, out long total, Direction.Ascending, null, null).FirstOrDefault(); + Assert.That(auditEntry, Is.Not.Null); + Assert.That(auditEntry.CreateDate, Is.Not.EqualTo(default(DateTime))); + } + [Test] public void Get_Paged_Items() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PublishedCache/ContentCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PublishedCache/ContentCacheTests.cs new file mode 100644 index 0000000000..6507bd6cda --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PublishedCache/ContentCacheTests.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PublishedCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class ContentCacheTests : UmbracoIntegrationTestWithContent +{ + private ContentStore GetContentStore() + { + var path = Path.Combine(GetRequiredService().LocalTempPath, "NuCache"); + Directory.CreateDirectory(path); + + var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); + var localContentDbExists = File.Exists(localContentDbPath); + var contentDataSerializer = new ContentDataSerializer(new DictionaryOfPropertyDataSerializer()); + var localContentDb = BTree.GetTree(localContentDbPath, localContentDbExists, new NuCacheSettings(), contentDataSerializer); + + return new ContentStore( + GetRequiredService(), + GetRequiredService(), + LoggerFactory.CreateLogger(), + LoggerFactory, + GetRequiredService(), // new NoopPublishedModelFactory + localContentDb); + } + + private ContentNodeKit CreateContentNodeKit() + { + var contentData = new ContentDataBuilder() + .WithName("Content 1") + .WithProperties(new PropertyDataBuilder() + .WithPropertyData("welcomeText", "Welcome") + .WithPropertyData("welcomeText", "Welcome", "en-US") + .WithPropertyData("welcomeText", "Willkommen", "de") + .WithPropertyData("welcomeText", "Welkom", "nl") + .WithPropertyData("welcomeText2", "Welcome") + .WithPropertyData("welcomeText2", "Welcome", "en-US") + .WithPropertyData("noprop", "xxx") + .Build()) + .Build(); + + return ContentNodeKitBuilder.CreateWithContent( + ContentType.Id, + 1, + "-1,1", + draftData: contentData, + publishedData: contentData); + } + + [Test] + public async Task SetLocked() + { + var contentStore = GetContentStore(); + + using (contentStore.GetScopedWriteLock(ScopeProvider)) + { + var contentNodeKit = CreateContentNodeKit(); + + contentStore.SetLocked(contentNodeKit); + + // Try running the same operation again in an async task + await Task.Run(() => contentStore.SetLocked(contentNodeKit)); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs index 0850422598..6ec212c02e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs @@ -6,7 +6,6 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; @@ -15,19 +14,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] public class SliderValueEditorTests { - // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( - private List _invalidValues = new(); - - [SetUp] - public void SetUp() => _invalidValues = new List + public static object[] InvalidCaseData = new object[] { 123m, 123, -123, - 123.45d, - "123.45", - "1.234,56", - "1.2.3.4", "something", true, new object(), @@ -36,21 +27,19 @@ public class SliderValueEditorTests new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()) }; - [Test] - public void Can_Handle_Invalid_Values_From_Editor() + [TestCaseSource(nameof(InvalidCaseData))] + public void Can_Handle_Invalid_Values_From_Editor(object value) { - foreach (var value in _invalidValues) - { - var fromEditor = FromEditor(value); - Assert.IsNull(fromEditor, message: $"Failed for: {value}"); - } + var fromEditor = FromEditor(value); + Assert.IsNull(fromEditor); } [TestCase("1", 1)] [TestCase("0", 0)] [TestCase("-1", -1)] [TestCase("123456789", 123456789)] - public void Can_Parse_Single_Value_To_Editor(string value, int expected) + [TestCase("123.45", 123.45)] + public void Can_Parse_Single_Value_To_Editor(string value, decimal expected) { var toEditor = ToEditor(value) as SliderPropertyEditor.SliderPropertyValueEditor.SliderRange; Assert.IsNotNull(toEditor); @@ -62,7 +51,10 @@ public class SliderValueEditorTests [TestCase("0,0", 0, 0)] [TestCase("-1,-1", -1, -1)] [TestCase("10,123456789", 10, 123456789)] - public void Can_Parse_Range_Value_To_Editor(string value, int expectedFrom, int expectedTo) + [TestCase("1.234,56", 1.234, 56)] + [TestCase("4,6.234", 4, 6.234)] + [TestCase("10.45,15.3", 10.45, 15.3)] + public void Can_Parse_Range_Value_To_Editor(string value, decimal expectedFrom, decimal expectedTo) { var toEditor = ToEditor(value) as SliderPropertyEditor.SliderPropertyValueEditor.SliderRange; Assert.IsNotNull(toEditor); @@ -75,21 +67,22 @@ public class SliderValueEditorTests [TestCase(0, 0, "0")] [TestCase(-10, -10, "-10")] [TestCase(10, 123456789, "10,123456789")] - public void Can_Parse_Valid_Value_From_Editor(int from, int to, string expectedResult) + [TestCase(1.5, 1.5, "1.5")] + [TestCase(0, 0.5, "0,0.5")] + [TestCase(5, 5.4, "5,5.4")] + [TestCase(0.5, 0.6, "0.5,0.6")] + public void Can_Parse_Valid_Value_From_Editor(decimal from, decimal to, string expectedResult) { var value = JsonNode.Parse($"{{\"from\": {from}, \"to\": {to}}}"); var fromEditor = FromEditor(value) as string; Assert.AreEqual(expectedResult, fromEditor); } - [Test] - public void Can_Handle_Invalid_Values_To_Editor() + [TestCaseSource(nameof(InvalidCaseData))] + public void Can_Handle_Invalid_Values_To_Editor(object value) { - foreach (var value in _invalidValues) - { - var toEditor = ToEditor(value); - Assert.IsNull(toEditor, message: $"Failed for: {value}"); - } + var toEditor = ToEditor(value); + Assert.IsNull(toEditor, message: $"Failed for: {value}"); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UriUtilityTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UriUtilityTests.cs index 51c9774027..0fb6ad6a28 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UriUtilityTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UriUtilityTests.cs @@ -23,7 +23,9 @@ public class UriUtilityTests // test that the trailing slash goes but not on hostname [TestCase("http://LocalHost/", "http://localhost/")] + [TestCase("http://LocalHost/////", "http://localhost/")] [TestCase("http://LocalHost/Home/", "http://localhost/home")] + [TestCase("http://LocalHost/Home/////", "http://localhost/home")] [TestCase("http://LocalHost/Home/?x=y", "http://localhost/home?x=y")] [TestCase("http://LocalHost/Home/Sub1/", "http://localhost/home/sub1")] [TestCase("http://LocalHost/Home/Sub1/?x=y", "http://localhost/home/sub1?x=y")]