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