diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs index b88147999a..4fed35a281 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs @@ -8,7 +8,6 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Controllers.Content; @@ -16,10 +15,11 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content; [ApiVersion("2.0")] public class ByRouteContentApiController : ContentApiItemControllerBase { - private readonly IRequestRoutingService _requestRoutingService; + private readonly IApiContentPathResolver _apiContentPathResolver; private readonly IRequestRedirectService _requestRedirectService; private readonly IRequestPreviewService _requestPreviewService; private readonly IRequestMemberAccessService _requestMemberAccessService; + private const string PreviewContentRequestPathPrefix = $"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}"; [Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")] public ByRouteContentApiController( @@ -58,7 +58,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase { } - [ActivatorUtilitiesConstructor] + [Obsolete($"Please use the constructor that accepts {nameof(IApiContentPathResolver)}. Will be removed in V15.")] public ByRouteContentApiController( IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilder, @@ -66,12 +66,50 @@ public class ByRouteContentApiController : ContentApiItemControllerBase IRequestRedirectService requestRedirectService, IRequestPreviewService requestPreviewService, IRequestMemberAccessService requestMemberAccessService) + : this( + apiPublishedContentCache, + apiContentResponseBuilder, + requestRedirectService, + requestPreviewService, + requestMemberAccessService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete($"Please use the non-obsolete constructor. Will be removed in V15.")] + public ByRouteContentApiController( + IApiPublishedContentCache apiPublishedContentCache, + IApiContentResponseBuilder apiContentResponseBuilder, + IPublicAccessService publicAccessService, + IRequestRoutingService requestRoutingService, + IRequestRedirectService requestRedirectService, + IRequestPreviewService requestPreviewService, + IRequestMemberAccessService requestMemberAccessService, + IApiContentPathResolver apiContentPathResolver) + : this( + apiPublishedContentCache, + apiContentResponseBuilder, + requestRedirectService, + requestPreviewService, + requestMemberAccessService, + apiContentPathResolver) + { + } + + [ActivatorUtilitiesConstructor] + public ByRouteContentApiController( + IApiPublishedContentCache apiPublishedContentCache, + IApiContentResponseBuilder apiContentResponseBuilder, + IRequestRedirectService requestRedirectService, + IRequestPreviewService requestPreviewService, + IRequestMemberAccessService requestMemberAccessService, + IApiContentPathResolver apiContentPathResolver) : base(apiPublishedContentCache, apiContentResponseBuilder) { - _requestRoutingService = requestRoutingService; _requestRedirectService = requestRedirectService; _requestPreviewService = requestPreviewService; _requestMemberAccessService = requestMemberAccessService; + _apiContentPathResolver = apiContentPathResolver; } [HttpGet("item/{*path}")] @@ -105,8 +143,6 @@ public class ByRouteContentApiController : ContentApiItemControllerBase private async Task HandleRequest(string path) { path = DecodePath(path); - - path = path.TrimStart("/"); path = path.Length == 0 ? "/" : path; IPublishedContent? contentItem = GetContent(path); @@ -128,17 +164,12 @@ public class ByRouteContentApiController : ContentApiItemControllerBase } private IPublishedContent? GetContent(string path) - => path.StartsWith(Constants.DeliveryApi.Routing.PreviewContentPathPrefix) + => path.StartsWith(PreviewContentRequestPathPrefix) ? GetPreviewContent(path) : GetPublishedContent(path); private IPublishedContent? GetPublishedContent(string path) - { - var contentRoute = _requestRoutingService.GetContentRoute(path); - - IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute); - return contentItem; - } + => _apiContentPathResolver.ResolveContentPath(path); private IPublishedContent? GetPreviewContent(string path) { @@ -147,7 +178,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase return null; } - if (Guid.TryParse(path.AsSpan(Constants.DeliveryApi.Routing.PreviewContentPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false) + if (Guid.TryParse(path.AsSpan(PreviewContentRequestPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false) { return null; } diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj index 260c5bec53..43bf47cb75 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentPathProvider.cs b/src/Umbraco.Core/DeliveryApi/ApiContentPathProvider.cs new file mode 100644 index 0000000000..853f0030ae --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/ApiContentPathProvider.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; + +namespace Umbraco.Cms.Core.DeliveryApi; + +// NOTE: left unsealed on purpose so it is extendable. +public class ApiContentPathProvider : IApiContentPathProvider +{ + private readonly IPublishedUrlProvider _publishedUrlProvider; + + public ApiContentPathProvider(IPublishedUrlProvider publishedUrlProvider) + => _publishedUrlProvider = publishedUrlProvider; + + public virtual string? GetContentPath(IPublishedContent content, string? culture) + => _publishedUrlProvider.GetUrl(content, UrlMode.Relative, culture); +} diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs b/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs new file mode 100644 index 0000000000..4ca6be3932 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs @@ -0,0 +1,26 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.DeliveryApi; + +// NOTE: left unsealed on purpose so it is extendable. +public class ApiContentPathResolver : IApiContentPathResolver +{ + private readonly IRequestRoutingService _requestRoutingService; + private readonly IApiPublishedContentCache _apiPublishedContentCache; + + public ApiContentPathResolver(IRequestRoutingService requestRoutingService, IApiPublishedContentCache apiPublishedContentCache) + { + _requestRoutingService = requestRoutingService; + _apiPublishedContentCache = apiPublishedContentCache; + } + + public virtual IPublishedContent? ResolveContentPath(string path) + { + path = path.EnsureStartsWith("/"); + + var contentRoute = _requestRoutingService.GetContentRoute(path); + IPublishedContent? contentItem = _apiPublishedContentCache.GetByRoute(contentRoute); + return contentItem; + } +} diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index 0ae310d585..148b466a8b 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -1,5 +1,7 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -10,13 +12,14 @@ namespace Umbraco.Cms.Core.DeliveryApi; public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder { - private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IApiContentPathProvider _apiContentPathProvider; private readonly GlobalSettings _globalSettings; private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IRequestPreviewService _requestPreviewService; private RequestHandlerSettings _requestSettings; + [Obsolete($"Use the constructor that does not accept {nameof(IPublishedUrlProvider)}. Will be removed in V15.")] public ApiContentRouteBuilder( IPublishedUrlProvider publishedUrlProvider, IOptions globalSettings, @@ -24,8 +27,32 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestPreviewService requestPreviewService, IOptionsMonitor requestSettings) + : this(StaticServiceProvider.Instance.GetRequiredService(), globalSettings, variationContextAccessor, publishedSnapshotAccessor, requestPreviewService, requestSettings) { - _publishedUrlProvider = publishedUrlProvider; + } + + [Obsolete($"Use the constructor that does not accept {nameof(IPublishedUrlProvider)}. Will be removed in V15.")] + public ApiContentRouteBuilder( + IPublishedUrlProvider publishedUrlProvider, + IApiContentPathProvider apiContentPathProvider, + IOptions globalSettings, + IVariationContextAccessor variationContextAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IRequestPreviewService requestPreviewService, + IOptionsMonitor requestSettings) + : this(apiContentPathProvider, globalSettings, variationContextAccessor, publishedSnapshotAccessor, requestPreviewService, requestSettings) + { + } + + public ApiContentRouteBuilder( + IApiContentPathProvider apiContentPathProvider, + IOptions globalSettings, + IVariationContextAccessor variationContextAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IRequestPreviewService requestPreviewService, + IOptionsMonitor requestSettings) + { + _apiContentPathProvider = apiContentPathProvider; _variationContextAccessor = variationContextAccessor; _publishedSnapshotAccessor = publishedSnapshotAccessor; _requestPreviewService = requestPreviewService; @@ -72,7 +99,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder } // grab the content path from the URL provider - var contentPath = _publishedUrlProvider.GetUrl(content, UrlMode.Relative, culture); + var contentPath = _apiContentPathProvider.GetContentPath(content, culture); // in some scenarios the published content is actually routable, but due to the built-in handling of i.e. lacking culture setup // the URL provider resolves the content URL as empty string or "#". since the Delivery API handles routing explicitly, @@ -96,7 +123,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private string ContentPreviewPath(IPublishedContent content) => $"{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{content.Key:D}{(_requestSettings.AddTrailingSlash ? "/" : string.Empty)}"; - private static bool IsInvalidContentPath(string path) => path.IsNullOrWhiteSpace() || "#".Equals(path); + private static bool IsInvalidContentPath(string? path) => path.IsNullOrWhiteSpace() || "#".Equals(path); private IPublishedContent GetRoot(IPublishedContent content, bool isPreview) { diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentPathProvider.cs b/src/Umbraco.Core/DeliveryApi/IApiContentPathProvider.cs new file mode 100644 index 0000000000..11a676873b --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiContentPathProvider.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiContentPathProvider +{ + string? GetContentPath(IPublishedContent content, string? culture); +} diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs b/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs new file mode 100644 index 0000000000..391f18998e --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiContentPathResolver +{ + IPublishedContent? ResolveContentPath(string path); +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs index e211d7c257..08088251e5 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs @@ -51,7 +51,7 @@ public class ExcessiveHeadersCheck : HealthCheck var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); // Access the site home page and check for the headers - var request = new HttpRequestMessage(HttpMethod.Head, url); + using var request = new HttpRequestMessage(HttpMethod.Head, url); try { using HttpResponseMessage response = await HttpClient.SendAsync(request); diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 888b136360..fbe5933b28 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -80,7 +80,7 @@ public class HttpsCheck : HealthCheck var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) { Scheme = Uri.UriSchemeHttps }; Uri url = urlBuilder.Uri; - var request = new HttpRequestMessage(HttpMethod.Head, url); + using var request = new HttpRequestMessage(HttpMethod.Head, url); try { diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs index ab580d6aae..d8efdab9d1 100644 --- a/src/Umbraco.Core/Logging/IProfiler.cs +++ b/src/Umbraco.Core/Logging/IProfiler.cs @@ -27,4 +27,9 @@ public interface IProfiler /// authenticated or you want to clear the results, based upon some other mechanism. /// void Stop(bool discardResults = false); + + /// + /// Whether the profiler is enabled. + /// + bool IsEnabled => true; } diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 84a57979bf..cc93372d75 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -35,6 +35,9 @@ public class LogProfiler : IProfiler // the log never stops } + /// + public bool IsEnabled => _logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug); + // a lightweight disposable timer private class LightDisposableTimer : DisposableObjectSlim { diff --git a/src/Umbraco.Core/Logging/NoopProfiler.cs b/src/Umbraco.Core/Logging/NoopProfiler.cs index 821728c7a6..c7bcf34bc0 100644 --- a/src/Umbraco.Core/Logging/NoopProfiler.cs +++ b/src/Umbraco.Core/Logging/NoopProfiler.cs @@ -14,6 +14,9 @@ public class NoopProfiler : IProfiler { } + /// + public bool IsEnabled => false; + private class VoidDisposable : DisposableObjectSlim { protected override void DisposeResources() diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs index 59d0f171ef..e53935f49e 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs @@ -60,7 +60,7 @@ public abstract class OEmbedProviderBase : IEmbedProvider using (var request = new HttpRequestMessage(HttpMethod.Get, url)) { - HttpResponseMessage response = _httpClient.SendAsync(request).GetAwaiter().GetResult(); + using HttpResponseMessage response = _httpClient.SendAsync(request).GetAwaiter().GetResult(); return response.Content.ReadAsStringAsync().Result; } } diff --git a/src/Umbraco.Core/Models/Link.cs b/src/Umbraco.Core/Models/Link.cs index 7047b54555..ca65f8850b 100644 --- a/src/Umbraco.Core/Models/Link.cs +++ b/src/Umbraco.Core/Models/Link.cs @@ -1,3 +1,5 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + namespace Umbraco.Cms.Core.Models; public class Link diff --git a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs index c30015a7a0..e79edec7f7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs @@ -20,7 +20,7 @@ public class InstallationRepository : IInstallationRepository _httpClient = new HttpClient(); } - var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); + using var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); await _httpClient.PostAsync(RestApiInstallUrl, content); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs index 4d4e642d9d..9cf0d52251 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs @@ -21,10 +21,10 @@ public class UpgradeCheckRepository : IUpgradeCheckRepository _httpClient = new HttpClient(); } - var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); + using var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); _httpClient.Timeout = TimeSpan.FromSeconds(1); - HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); + using HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); var json = await task.Content.ReadAsStringAsync(); UpgradeResult? result = _jsonSerializer.Deserialize(json); diff --git a/src/Umbraco.Core/PublishedCache/ITagQuery.cs b/src/Umbraco.Core/PublishedCache/ITagQuery.cs index 2deaf75108..e0c6a135c9 100644 --- a/src/Umbraco.Core/PublishedCache/ITagQuery.cs +++ b/src/Umbraco.Core/PublishedCache/ITagQuery.cs @@ -33,6 +33,11 @@ public interface ITagQuery /// /// Gets all document tags. /// + /// /// + /// If no culture is specified, it retrieves tags with an invariant culture. + /// If a culture is specified, it only retrieves tags for that culture. + /// Use "*" to retrieve tags for all cultures. + /// IEnumerable GetAllContentTags(string? group = null, string? culture = null); /// diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index a960fd7998..f50d0b5bdb 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -16,8 +16,11 @@ public class LegacyPasswordSecurity public static string GenerateSalt() { var numArray = new byte[16]; - new RNGCryptoServiceProvider().GetBytes(numArray); - return Convert.ToBase64String(numArray); + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(numArray); + return Convert.ToBase64String(numArray); + } } // TODO: Remove v11 @@ -86,7 +89,7 @@ public class LegacyPasswordSecurity /// public bool VerifyLegacyHashedPassword(string password, string dbPassword) { - var hashAlgorithm = new HMACSHA1 + using var hashAlgorithm = new HMACSHA1 { // the legacy salt was actually the password :( Key = Encoding.Unicode.GetBytes(password), diff --git a/src/Umbraco.Core/Security/PasswordGenerator.cs b/src/Umbraco.Core/Security/PasswordGenerator.cs index 7d55e0e39d..1d938d36c8 100644 --- a/src/Umbraco.Core/Security/PasswordGenerator.cs +++ b/src/Umbraco.Core/Security/PasswordGenerator.cs @@ -98,7 +98,10 @@ public class PasswordGenerator var data = new byte[length]; var chArray = new char[length]; var num1 = 0; - new RNGCryptoServiceProvider().GetBytes(data); + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(data); + } for (var index = 0; index < length; ++index) { diff --git a/src/Umbraco.Core/Webhooks/Events/HealthCheck/HealthCheckCompletedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/HealthCheck/HealthCheckCompletedWebhookEvent.cs index b38cb8d22b..e94fc97bcc 100644 --- a/src/Umbraco.Core/Webhooks/Events/HealthCheck/HealthCheckCompletedWebhookEvent.cs +++ b/src/Umbraco.Core/Webhooks/Events/HealthCheck/HealthCheckCompletedWebhookEvent.cs @@ -15,5 +15,10 @@ public class HealthCheckCompletedWebhookEvent : WebhookEventBase Constants.WebhookEvents.Aliases.HealthCheckCompleted; - public override object? ConvertNotificationToRequestPayload(HealthCheckCompletedNotification notification) => notification.HealthCheckResults; + public override object? ConvertNotificationToRequestPayload(HealthCheckCompletedNotification notification) => + new + { + notification.HealthCheckResults.AllChecksSuccessful, + notification.HealthCheckResults.ResultsAsDictionary + }; } diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs index 87d4d459fa..a3b194226c 100644 --- a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -136,12 +136,12 @@ public class UmbracoXPathPathSyntaxParser }); } - // These parameters must have a node id context - if (nodeContextId.HasValue) + if (nodeContextId.HasValue || parentId.HasValue) { + var currentId = nodeContextId.HasValue && nodeContextId.Value != default ? nodeContextId.Value : parentId.GetValueOrDefault(); vars.Add("$current", q => { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(currentId)); return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); }); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 58d3c5493d..af45471080 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -424,6 +424,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 8ebd803aac..85fa7ad38c 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -43,8 +43,8 @@ public class UmbracoMapper : IUmbracoMapper // note // // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary - // the inner dictionaries are never modified and therefore can be simple Dictionary - private readonly ConcurrentDictionary>> _ctors = + // the inner dictionaries can also be modified, see GetCtor and therefore also needs to be a ConcurrentDictionary + private readonly ConcurrentDictionary>> _ctors = new(); private readonly ConcurrentDictionary>> _maps = @@ -129,7 +129,7 @@ public class UmbracoMapper : IUmbracoMapper Type sourceType = typeof(TSource); Type targetType = typeof(TTarget); - Dictionary> sourceCtors = DefineCtors(sourceType); + ConcurrentDictionary> sourceCtors = DefineCtors(sourceType); if (ctor != null) { sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; @@ -139,8 +139,8 @@ public class UmbracoMapper : IUmbracoMapper sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); } - private Dictionary> DefineCtors(Type sourceType) => - _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); + private ConcurrentDictionary> DefineCtors(Type sourceType) => + _ctors.GetOrAdd(sourceType, _ => new ConcurrentDictionary>()); private ConcurrentDictionary> DefineMaps(Type sourceType) => _maps.GetOrAdd(sourceType, _ => new ConcurrentDictionary>()); @@ -391,7 +391,7 @@ public class UmbracoMapper : IUmbracoMapper return null; } - if (_ctors.TryGetValue(sourceType, out Dictionary>? sourceCtor) && + if (_ctors.TryGetValue(sourceType, out ConcurrentDictionary>? sourceCtor) && sourceCtor.TryGetValue(targetType, out Func? ctor)) { return ctor; @@ -399,7 +399,7 @@ public class UmbracoMapper : IUmbracoMapper // we *may* run this more than once but it does not matter ctor = null; - foreach ((Type stype, Dictionary> sctors) in _ctors) + foreach ((Type stype, ConcurrentDictionary> sctors) in _ctors) { if (!stype.IsAssignableFrom(sourceType)) { @@ -427,7 +427,7 @@ public class UmbracoMapper : IUmbracoMapper { if (!v.ContainsKey(c.Key)) { - v.Add(c.Key, c.Value); + v.TryAdd(c.Key, c.Value); } } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index a01b6f0988..270cf5e54a 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -38,6 +38,7 @@ public class WebProfiler : IProfiler public IDisposable? Step(string name) => MiniProfiler.Current?.Step(name); + bool IsEnabled => true; public void Start() { diff --git a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs index f4f2f004e7..5862f90d64 100644 --- a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs +++ b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs @@ -19,7 +19,7 @@ public class ProfilingViewEngine : IViewEngine public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) { - using (_profiler.Step(string.Format("{0}.FindView, {1}, {2}", _name, viewName, isMainPage))) + using (_profiler.IsEnabled ? _profiler.Step(string.Format("{0}.FindView, {1}, {2}", _name, viewName, isMainPage)) : null) { return WrapResult(Inner.FindView(context, viewName, isMainPage)); } @@ -27,7 +27,7 @@ public class ProfilingViewEngine : IViewEngine public ViewEngineResult GetView(string? executingFilePath, string viewPath, bool isMainPage) { - using (_profiler.Step(string.Format("{0}.GetView, {1}, {2}, {3}", _name, executingFilePath, viewPath, isMainPage))) + using (_profiler.IsEnabled ? _profiler.Step(string.Format("{0}.GetView, {1}, {2}, {3}", _name, executingFilePath, viewPath, isMainPage)) : null) { return Inner.GetView(executingFilePath, viewPath, isMainPage); } diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index a435cf2487..ccd28ece91 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -387,9 +387,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { diff --git a/tests/Umbraco.Tests.Common/TestHelpers/Stubs/TestProfiler.cs b/tests/Umbraco.Tests.Common/TestHelpers/Stubs/TestProfiler.cs index 097201f4fa..f0fc41a584 100644 --- a/tests/Umbraco.Tests.Common/TestHelpers/Stubs/TestProfiler.cs +++ b/tests/Umbraco.Tests.Common/TestHelpers/Stubs/TestProfiler.cs @@ -39,6 +39,9 @@ public class TestProfiler : IProfiler } } + /// + public bool IsEnabled => _enabled; + public static void Enable() => _enabled = true; public static void Disable() => _enabled = false; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index 6e0dfd7e6f..cbe8ee09b2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -31,12 +31,12 @@ public class ContentBuilderTests : DeliveryApiTests content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 06, 01)); content.SetupGet(c => c.UpdateDate).Returns(new DateTime(2023, 07, 12)); - var publishedUrlProvider = new Mock(); - publishedUrlProvider - .Setup(p => p.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((IPublishedContent content, UrlMode mode, string? culture, Uri? current) => $"url:{content.UrlSegment}"); + var apiContentRouteProvider = new Mock(); + apiContentRouteProvider + .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) + .Returns((IPublishedContent c, string? culture) => $"url:{c.UrlSegment}"); - var routeBuilder = CreateContentRouteBuilder(publishedUrlProvider.Object, CreateGlobalSettings()); + var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings()); var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor()); var result = builder.Build(content.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs index 61e55b76db..9e34979c6e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs @@ -18,7 +18,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests PublishedSnapshotAccessor, new ApiContentBuilder( nameProvider ?? new ApiContentNameProvider(), - CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()), + CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()), CreateOutputExpansionStrategyAccessor())); [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index d60097bf69..8fafb162a2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -254,6 +254,34 @@ public class ContentRouteBuilderTests : DeliveryApiTests } } + [Test] + public void CanUseCustomContentPathProvider() + { + var rootKey = Guid.NewGuid(); + var root = SetupInvariantPublishedContent("The Root", rootKey, published: false); + + var childKey = Guid.NewGuid(); + var child = SetupInvariantPublishedContent("The Child", childKey, root); + + var apiContentPathProvider = new Mock(); + apiContentPathProvider + .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) + .Returns((IPublishedContent content, string? culture) => $"my-custom-path-for-{content.UrlSegment}"); + + var builder = CreateApiContentRouteBuilder(true, apiContentPathProvider: apiContentPathProvider.Object); + var result = builder.Build(root); + Assert.NotNull(result); + Assert.AreEqual("/my-custom-path-for-the-root", result.Path); + Assert.AreEqual(rootKey, result.StartItem.Id); + Assert.AreEqual("the-root", result.StartItem.Path); + + result = builder.Build(child); + Assert.NotNull(result); + Assert.AreEqual("/my-custom-path-for-the-child", result.Path); + Assert.AreEqual(rootKey, result.StartItem.Id); + Assert.AreEqual("the-root", result.StartItem.Path); + } + private IPublishedContent SetupInvariantPublishedContent(string name, Guid key, IPublishedContent? parent = null, bool published = true) { var publishedContentType = CreatePublishedContentType(); @@ -310,7 +338,10 @@ public class ContentRouteBuilderTests : DeliveryApiTests return publishedUrlProvider.Object; } - private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) + private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath) + => new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath)); + + private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null, IApiContentPathProvider? apiContentPathProvider = null) { var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash }; var requestHandlerSettingsMonitorMock = new Mock>(); @@ -320,9 +351,10 @@ public class ContentRouteBuilderTests : DeliveryApiTests requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); publishedSnapshotAccessor ??= CreatePublishedSnapshotAccessorForRoute("#"); + apiContentPathProvider ??= SetupApiContentPathProvider(hideTopLevelNodeFromPath); return CreateContentRouteBuilder( - SetupPublishedUrlProvider(hideTopLevelNodeFromPath), + apiContentPathProvider, CreateGlobalSettings(hideTopLevelNodeFromPath), requestHandlerSettingsMonitor: requestHandlerSettingsMonitorMock.Object, requestPreviewService: requestPreviewServiceMock.Object, @@ -335,12 +367,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests publishedUrlProviderMock .Setup(p => p.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(publishedUrl); + var contentPathProvider = new ApiContentPathProvider(publishedUrlProviderMock.Object); var publishedSnapshotAccessor = CreatePublishedSnapshotAccessorForRoute(routeById); var content = SetupVariantPublishedContent("The Content", Guid.NewGuid()); var builder = CreateContentRouteBuilder( - publishedUrlProviderMock.Object, + contentPathProvider, CreateGlobalSettings(), publishedSnapshotAccessor: publishedSnapshotAccessor); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 80733e3cdd..47a7c032c9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -114,7 +114,7 @@ public class DeliveryApiTests => $"{name.ToLowerInvariant().Replace(" ", "-")}{(culture.IsNullOrWhiteSpace() ? string.Empty : $"-{culture}")}"; protected ApiContentRouteBuilder CreateContentRouteBuilder( - IPublishedUrlProvider publishedUrlProvider, + IApiContentPathProvider contentPathProvider, IOptions globalSettings, IVariationContextAccessor? variationContextAccessor = null, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null, @@ -129,7 +129,7 @@ public class DeliveryApiTests } return new ApiContentRouteBuilder( - publishedUrlProvider, + contentPathProvider, globalSettings, variationContextAccessor ?? Mock.Of(), publishedSnapshotAccessor ?? Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index b38aa33c8a..1948756e44 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -21,7 +21,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var contentNameProvider = new ApiContentNameProvider(); var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider); - routeBuilder = routeBuilder ?? CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); + routeBuilder = routeBuilder ?? CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiNodeTreePickerValueConverter( PublishedSnapshotAccessor, Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs index 74f269f0ea..f4a565ec4f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs @@ -294,7 +294,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests private MultiUrlPickerValueConverter MultiUrlPickerValueConverter() { - var routeBuilder = CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); + var routeBuilder = CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiUrlPickerValueConverter( PublishedSnapshotAccessor, Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs index 90f78a225e..06d8aad378 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs @@ -455,5 +455,5 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None); } - protected IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); + protected IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs index d3831ec2e4..475945c9df 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs @@ -1,5 +1,6 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; @@ -12,6 +13,8 @@ public class PropertyValueConverterTests : DeliveryApiTests protected IPublishedUrlProvider PublishedUrlProvider { get; private set; } + protected IApiContentPathProvider ApiContentPathProvider { get; private set; } + protected IPublishedContent PublishedContent { get; private set; } protected IPublishedContent PublishedMedia { get; private set; } @@ -69,6 +72,7 @@ public class PropertyValueConverterTests : DeliveryApiTests .Setup(p => p.GetMediaUrl(publishedMedia.Object, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns("the-media-url"); PublishedUrlProvider = PublishedUrlProviderMock.Object; + ApiContentPathProvider = new ApiContentPathProvider(PublishedUrlProvider); var publishedSnapshotAccessor = new Mock(); var publishedSnapshotObject = publishedSnapshot.Object;