From a4a9a274bb825290f25e3313511511c95abff93e Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 22 May 2024 12:05:41 +0200 Subject: [PATCH] V14: Merge 13 to 14 (#16340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) * Ported over #15928 changes for 13.3 RC (#16023) * Ported over #15928 changes for 13.3 RC * Use GetOrAdd() * Lock dictionary initialization --------- Co-authored-by: Jason Elkin * Make the API content response builder extendable (#16056) * Make the API content response builder extendable * DeliveryApiJsonTypeResolver needs to be extendable too * bump rc to regular * Bump to next minor * Add blocks in RTE telemetry (#16104) * Add blocks telemetry * Use constants and update tests * V13: Add property type information to telemetry (#16109) * Add property type counts to telemetry * Use constants and fix tests * Update description * V10: Fix for fallback file upload (#14892) (#15868) * Fix for fallback file upload (#14892) * Added check for file type * Removed unneeded null checks and fixed tabs * Cleaning * Cleanups, cleanups, and removal of unneeded null checks * Reverted removal of relationshipservice * Revert null check removals (too risky) --------- Co-authored-by: Ambert van Unen Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com> (cherry picked from commit 0b5d1f8aa60ca92f63d68e21cc1787a379e33895) * Fix up formatting --------- Co-authored-by: Ambert van Unen * Implementors using Umbraco.Tests.Integration won't have to override GetLocalizedTextService (cherry picked from commit b0016687eb583a549da8992f5fba92e269b4cbfa) (cherry picked from commit 2bb56f1b81a84df3ef03ba0170f741cea6007c28) * Fix logic for retrieving lastKnownElement (cherry picked from commit cae106bfe8fa11c080fc90ba4354a0791e47f9a2) * bump version * Bump version * Bump version * Since v13 properties can sometimes be of type IRichTextEditorIntermediateValue - this was unexpected in the XPath navigator code (#16121) * Webhook log improvements (#16200) * fix: include all headers in webhook log * feat: return webhook log status from server * feat: make webhook logs deep linkable * feat: add webhook log pagination * feat: improve webhook request/response body preview * V13: Optimize custom MVC routing (#16218) * Introduce EagerMatcherPolicy to conditionally bypass content routing * Ensure that the candidate we disable dynamic routing for is valid * Skip Umbraco endpoints * Simplify logic a bit * Move install logic to matcher * Ensure that dynamic routing is still skipped when in upgrade state * Fixup comments * Reduce nesting a bit * Don't show maintenance page when statically routed controllers are hít * Remove excess check, since installer requests are statically routed * V13: Optimize custom MVC routing (#16218) * Introduce EagerMatcherPolicy to conditionally bypass content routing * Ensure that the candidate we disable dynamic routing for is valid * Skip Umbraco endpoints * Simplify logic a bit * Move install logic to matcher * Ensure that dynamic routing is still skipped when in upgrade state * Fixup comments * Reduce nesting a bit * Don't show maintenance page when statically routed controllers are hít * Remove excess check, since installer requests are statically routed (cherry picked from commit ba9ddd11da66b6ecdf72fdbfc660234d63843bb8) * Property source level variation should only be applied when configured (#16270) * Property source level variation should only be applied when configured (#16270) (cherry picked from commit ab32bac5d96695eebe4f333a9bd6fa8d8b820f71) * Merge pull request from GHSA-j74q-mv2c-rxmp * Merge pull request from GHSA-j74q-mv2c-rxmp * Merge pull request from GHSA-j74q-mv2c-rxmp * Fix up after merge * Remove obselete test --------- Co-authored-by: Andy Butland Co-authored-by: Kenn Jacobsen Co-authored-by: Jason Elkin Co-authored-by: Sven Geusens Co-authored-by: Mole Co-authored-by: Ambert van Unen Co-authored-by: Lars-Erik Co-authored-by: Joshua Daniel Pratt Nielsen Co-authored-by: Bjarke Berg Co-authored-by: Sebastiaan Janssen Co-authored-by: Rasmus John Pedersen --- .../Json/DeliveryApiJsonTypeResolver.cs | 35 ++- src/Umbraco.Core/Constants-Telemetry.cs | 5 + src/Umbraco.Core/Constants-Web.cs | 1 + .../DeliveryApi/ApiContentResponseBuilder.cs | 11 +- .../EmbeddedResources/Lang/en.xml | 2 +- .../EmbeddedResources/Lang/en_us.xml | 2 +- src/Umbraco.Core/Models/WebhookLog.cs | 2 + .../Routing/UmbracoRequestPaths.cs | 2 +- src/Umbraco.Core/Routing/WebPath.cs | 24 ++ .../Services/WebhookLogFactory.cs | 5 +- .../UmbracoBuilder.TelemetryProviders.cs | 1 + .../Factories/WebhookLogFactory.cs | 4 +- .../BlocksInRichTextTelemetryProvider.cs | 42 ++++ .../PropertyEditorTelemetryProvider.cs | 8 + .../Property.cs | 177 +++++++------- .../UmbracoBuilderExtensions.cs | 1 + .../UmbracoApplicationBuilder.Website.cs | 3 +- .../Routing/EagerMatcherPolicy.cs | 228 ++++++++++++++++++ .../Routing/UmbracoRouteValueTransformer.cs | 29 --- .../Telemetry/TelemetryServiceTests.cs | 5 + .../PublishedContentVarianceTests.cs | 22 ++ .../Routing/UmbracoRequestPathsTests.cs | 17 -- .../Umbraco.Core/Routing/WebPathTests.cs | 83 +++++++ 23 files changed, 556 insertions(+), 153 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs create mode 100644 src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs index 10f052485a..b22e7c9341 100644 --- a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs +++ b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs @@ -12,23 +12,36 @@ public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver { JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); - if (jsonTypeInfo.Type == typeof(IApiContent)) + Type[] derivedTypes = GetDerivedTypes(jsonTypeInfo); + if (derivedTypes.Length > 0) { - ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContent)); - } - else if (jsonTypeInfo.Type == typeof(IApiContentResponse)) - { - ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContentResponse)); - } - else if (jsonTypeInfo.Type == typeof(IRichTextElement)) - { - ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement)); + ConfigureJsonPolymorphismOptions(jsonTypeInfo, derivedTypes); } return jsonTypeInfo; } - private void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes) + protected virtual Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo) + { + if (jsonTypeInfo.Type == typeof(IApiContent)) + { + return new[] { typeof(ApiContent) }; + } + + if (jsonTypeInfo.Type == typeof(IApiContentResponse)) + { + return new[] { typeof(ApiContentResponse) }; + } + + if (jsonTypeInfo.Type == typeof(IRichTextElement)) + { + return new[] { typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement) }; + } + + return Array.Empty(); + } + + protected void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes) { jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions { diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index b054f78be3..f1c6a6c52a 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -35,5 +35,10 @@ public static partial class Constants public static string WebhookTotal = $"{WebhookPrefix}Total"; public static string WebhookCustomHeaders = $"{WebhookPrefix}CustomHeaders"; public static string WebhookCustomEvent = $"{WebhookPrefix}CustomEvent"; + public static string RichTextEditorCount = "RichTextEditorCount"; + public static string RichTextBlockCount = "RichTextBlockCount"; + public static string TotalPropertyCount = "TotalPropertyCount"; + public static string HighestPropertyCount = "HighestPropertyCount"; + public static string TotalCompositions = "TotalCompositions"; } } diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index acd1bb50ef..283ec6eaa3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -62,6 +62,7 @@ public static partial class Constants public const string ControllerToken = "controller"; public const string ActionToken = "action"; public const string AreaToken = "area"; + public const string DynamicRoutePattern = "/{**umbracoSlug}"; } public static class RoutePath diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs index a551115a1e..68bb01c012 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs @@ -1,11 +1,10 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DeliveryApi; -public sealed class ApiContentResponseBuilder : ApiContentBuilderBase, IApiContentResponseBuilder +public class ApiContentResponseBuilder : ApiContentBuilderBase, IApiContentResponseBuilder { private readonly IApiContentRouteBuilder _apiContentRouteBuilder; @@ -14,6 +13,12 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase _apiContentRouteBuilder = apiContentRouteBuilder; protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties) + { + IDictionary cultures = GetCultures(content); + return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, cultures); + } + + protected virtual IDictionary GetCultures(IPublishedContent content) { var routesByCulture = new Dictionary(); @@ -35,6 +40,6 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase
  • Anonymized site ID, Umbraco version, and packages installed.
  • -
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, and Property Editors in use.
  • +
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Property Types, Compositions, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, rich text datatypes, blocks used in rich text datatypes, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
  • diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 60c3e7dfa9..01482e3863 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -470,7 +470,7 @@ We will send:
    • Anonymized site ID, Umbraco version, and packages installed.
    • -
    • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
    • +
    • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Property Types, Compositions, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, rich text datatypes, blocks used in rich text datatypes, and Property Editors in use.
    • System information: Webserver, server OS, server framework, server OS language, and database provider.
    • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
    diff --git a/src/Umbraco.Core/Models/WebhookLog.cs b/src/Umbraco.Core/Models/WebhookLog.cs index aba3b15713..5340b0f738 100644 --- a/src/Umbraco.Core/Models/WebhookLog.cs +++ b/src/Umbraco.Core/Models/WebhookLog.cs @@ -27,4 +27,6 @@ public class WebhookLog public string ResponseBody { get; set; } = string.Empty; public bool ExceptionOccured { get; set; } + + public bool IsSuccessStatusCode { get; set; } } diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index 82416c4f41..a5083a77c9 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -143,7 +143,7 @@ public class UmbracoRequestPaths /// /// Checks if the current uri is an install request /// - public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); + public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_managementApiPath); /// /// Rudimentary check to see if it's not a server side request diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index 7ecafff8a3..47c6b15871 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -50,4 +50,28 @@ public class WebPath return sb.ToString(); } + + + /// + /// Determines whether the provided web path is well-formed according to the specified UriKind. + /// + /// The web path to check. This can be null. + /// The kind of Uri (Absolute, Relative, or RelativeOrAbsolute). + /// + /// true if is well-formed; otherwise, false. + /// + public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind) + { + if (string.IsNullOrWhiteSpace(webPath)) + { + return false; + } + + if (webPath.StartsWith("//")) + { + return uriKind is not UriKind.Relative; + } + + return Uri.IsWellFormedUriString(webPath, uriKind); + } } diff --git a/src/Umbraco.Core/Services/WebhookLogFactory.cs b/src/Umbraco.Core/Services/WebhookLogFactory.cs index 2c80f15e27..3c260c017f 100644 --- a/src/Umbraco.Core/Services/WebhookLogFactory.cs +++ b/src/Umbraco.Core/Services/WebhookLogFactory.cs @@ -16,7 +16,7 @@ public class WebhookLogFactory : IWebhookLogFactory Url = webhook.Url, WebhookKey = webhook.Key, RetryCount = retryCount, - RequestHeaders = requestMessage.Headers.ToString(), + RequestHeaders = $"{requestMessage.Content?.Headers}{requestMessage.Headers}", RequestBody = await requestMessage.Content?.ReadAsStringAsync(cancellationToken)!, ExceptionOccured = exception is not null, }; @@ -24,7 +24,8 @@ public class WebhookLogFactory : IWebhookLogFactory if (httpResponseMessage is not null) { log.StatusCode = MapStatusCodeToMessage(httpResponseMessage.StatusCode); - log.ResponseHeaders = httpResponseMessage.Headers.ToString(); + log.IsSuccessStatusCode = httpResponseMessage.IsSuccessStatusCode; + log.ResponseHeaders = $"{httpResponseMessage.Content.Headers}{httpResponseMessage.Headers}"; log.ResponseBody = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); } else if (exception is HttpRequestException httpRequestException) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index a498ee7d05..fd5468ceda 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -20,6 +20,7 @@ public static class UmbracoBuilder_TelemetryProviders builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs index f060525624..886ee58eaf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Models; +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -33,6 +34,7 @@ internal static class WebhookLogFactory ResponseBody = dto.ResponseBody, RetryCount = dto.RetryCount, StatusCode = dto.StatusCode, + IsSuccessStatusCode = Regex.IsMatch(dto.StatusCode, "^.*\\(2(\\d{2})\\)$"), Key = dto.Key, Id = dto.Id, Url = dto.Url, diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs new file mode 100644 index 0000000000..89e2b998d6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers; + +public class BlocksInRichTextTelemetryProvider : IDetailedTelemetryProvider +{ + private readonly IDataTypeService _dataTypeService; + + public BlocksInRichTextTelemetryProvider(IDataTypeService dataTypeService) + { + _dataTypeService = dataTypeService; + } + + public IEnumerable GetInformation() + { + IEnumerable richTextDataTypes = _dataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.RichText).GetAwaiter().GetResult().ToArray(); + int registeredBlocks = 0; + yield return new UsageInformation(Constants.Telemetry.RichTextEditorCount, richTextDataTypes.Count()); + + foreach (IDataType richTextDataType in richTextDataTypes) + { + if (richTextDataType.ConfigurationObject is not RichTextConfiguration richTextConfiguration) + { + // Might be some custom data type, skip it + continue; + } + + if (richTextConfiguration.Blocks is null) + { + continue; + } + + registeredBlocks += richTextConfiguration.Blocks.Length; + } + + yield return new UsageInformation(Constants.Telemetry.RichTextBlockCount, registeredBlocks); + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs index 1c8e0af1ab..8bd735ec1c 100644 --- a/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs @@ -16,11 +16,19 @@ public class PropertyEditorTelemetryProvider : IDetailedTelemetryProvider { IEnumerable contentTypes = _contentTypeService.GetAll(); var propertyTypes = new HashSet(); + var propertyTypeCounts = new List(); + var totalCompositions = 0; + foreach (IContentType contentType in contentTypes) { propertyTypes.UnionWith(contentType.PropertyTypes.Select(x => x.PropertyEditorAlias)); + propertyTypeCounts.Add(contentType.CompositionPropertyTypes.Count()); + totalCompositions += contentType.CompositionAliases().Count(); } yield return new UsageInformation(Constants.Telemetry.Properties, propertyTypes); + yield return new UsageInformation(Constants.Telemetry.TotalPropertyCount, propertyTypeCounts.Sum()); + yield return new UsageInformation(Constants.Telemetry.HighestPropertyCount, propertyTypeCounts.Count > 0 ? propertyTypeCounts.Max() : 0); + yield return new UsageInformation(Constants.Telemetry.TotalCompositions, totalCompositions); } } diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index aa30fc9570..afe3b54484 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Xml.Serialization; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Collections; @@ -19,13 +20,12 @@ internal class Property : PublishedPropertyBase private readonly bool _isMember; private readonly bool _isPreviewing; - private readonly object _locko = new(); private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; // the invariant-neutral source and inter values private readonly object? _sourceValue; private readonly ContentVariation _variations; - private bool _sourceValueIsInvariant; + private readonly ContentVariation _sourceVariations; // the variant and non-variant object values private CacheValues? _cacheValues; @@ -33,7 +33,8 @@ internal class Property : PublishedPropertyBase private object? _interValue; // the variant source and inter values - private Dictionary? _sourceValues; + private readonly object _locko = new(); + private ConcurrentDictionary? _sourceValues; private string? _valuesCacheKey; @@ -66,12 +67,9 @@ internal class Property : PublishedPropertyBase } else { - if (_sourceValues == null) - { - _sourceValues = new Dictionary(); - } + EnsureSourceValuesInitialized(); - _sourceValues[new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)] + _sourceValues![new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)] = new SourceInterValue { Culture = sourceValue.Culture, @@ -90,7 +88,7 @@ internal class Property : PublishedPropertyBase // this variable is used for contextualizing the variation level when calculating property values. // it must be set to the union of variance (the combination of content type and property type variance). _variations = propertyType.Variations | content.ContentType.Variations; - _sourceValueIsInvariant = propertyType.Variations is ContentVariation.Nothing; + _sourceVariations = propertyType.Variations; } // clone for previewing as draft a published content that is published and has no draft @@ -106,7 +104,7 @@ internal class Property : PublishedPropertyBase _isMember = origin._isMember; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; _variations = origin._variations; - _sourceValueIsInvariant = origin._sourceValueIsInvariant; + _sourceVariations = origin._sourceVariations; } // used to cache the CacheValues of this property @@ -125,54 +123,53 @@ internal class Property : PublishedPropertyBase return hasValue.Value; } - lock (_locko) + value = GetInterValue(culture, segment); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) { - value = GetInterValue(culture, segment); - hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); - if (hasValue.HasValue) - { - return hasValue.Value; - } - - CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - if (!cacheValues.ObjectInitialized) - { - cacheValues.ObjectValue = - PropertyType.ConvertInterToObject(_content, initialCacheLevel, value, _isPreviewing); - cacheValues.ObjectInitialized = true; - } - - value = cacheValues.ObjectValue; - return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + return hasValue.Value; } + + CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (!cacheValues.ObjectInitialized) + { + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(_content, initialCacheLevel, value, _isPreviewing); + cacheValues.ObjectInitialized = true; + } + + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; } public override object? GetSourceValue(string? culture = null, string? segment = null) { - _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, ref culture, ref segment); - if (_sourceValueIsInvariant || (culture == string.Empty && segment == string.Empty)) + // source values are tightly bound to the property/schema culture and segment configurations, so we need to + // sanitize the contextualized culture/segment states before using them to access the source values. + culture = _sourceVariations.VariesByCulture() ? culture : string.Empty; + segment = _sourceVariations.VariesBySegment() ? segment : string.Empty; + + if (culture == string.Empty && segment == string.Empty) { return _sourceValue; } - lock (_locko) + if (_sourceValues == null) { - if (_sourceValues == null) - { - return null; - } - - return _sourceValues.TryGetValue( - new CompositeStringStringKey(culture, segment), - out SourceInterValue? sourceValue) - ? sourceValue.SourceValue - : null; + return null; } + + return _sourceValues.TryGetValue( + new CompositeStringStringKey(culture, segment), + out SourceInterValue? sourceValue) + ? sourceValue.SourceValue + : null; } private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) @@ -227,7 +224,6 @@ internal class Property : PublishedPropertyBase return (CacheValues)cache.Get(ValuesCacheKey, () => new CacheValues())!; } - // this is always invoked from within a lock, so does not require its own lock private object? GetInterValue(string? culture, string? segment) { if (culture == string.Empty && segment == string.Empty) @@ -242,21 +238,17 @@ internal class Property : PublishedPropertyBase return _interValue; } - if (_sourceValues == null) - { - _sourceValues = new Dictionary(); - } + EnsureSourceValuesInitialized(); var k = new CompositeStringStringKey(culture, segment); - if (!_sourceValues.TryGetValue(k, out SourceInterValue? vvalue)) - { - _sourceValues[k] = vvalue = new SourceInterValue + + SourceInterValue vvalue = _sourceValues!.GetOrAdd(k, _ => + new SourceInterValue { Culture = culture, Segment = segment, SourceValue = GetSourceValue(culture, segment), - }; - } + }); if (vvalue.InterInitialized) { @@ -273,23 +265,20 @@ internal class Property : PublishedPropertyBase _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); object? value; - lock (_locko) + CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (cacheValues.ObjectInitialized) { - CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - if (cacheValues.ObjectInitialized) - { - return cacheValues.ObjectValue; - } - - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); - cacheValues.ObjectInitialized = true; - value = cacheValues.ObjectValue; + return cacheValues.ObjectValue; } + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); + cacheValues.ObjectInitialized = true; + value = cacheValues.ObjectValue; + return value; } @@ -298,18 +287,16 @@ internal class Property : PublishedPropertyBase _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); object? value; - lock (_locko) - { - CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment); + CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment); - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding); - value = expanding - ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) - : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); - } + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding); + value = expanding + ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) + : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); return value; } @@ -359,9 +346,9 @@ internal class Property : PublishedPropertyBase private class CacheValues : CacheValue { - private Dictionary? _values; + private readonly object _locko = new(); + private ConcurrentDictionary? _values; - // this is always invoked from within a lock, so does not require its own lock public CacheValue For(string? culture, string? segment) { if (culture == string.Empty && segment == string.Empty) @@ -371,14 +358,15 @@ internal class Property : PublishedPropertyBase if (_values == null) { - _values = new Dictionary(); + lock (_locko) + { + _values ??= InitializeConcurrentDictionary(); + } } var k = new CompositeStringStringKey(culture, segment); - if (!_values.TryGetValue(k, out CacheValue? value)) - { - _values[k] = value = new CacheValue(); - } + + CacheValue value = _values.GetOrAdd(k, _ => new CacheValue()); return value; } @@ -408,5 +396,22 @@ internal class Property : PublishedPropertyBase public object? InterValue { get; set; } } + private static ConcurrentDictionary InitializeConcurrentDictionary() + where TKey : notnull + => new(-1, 5); + + private void EnsureSourceValuesInitialized() + { + if (_sourceValues is not null) + { + return; + } + + lock (_locko) + { + _sourceValues ??= InitializeConcurrentDictionary(); + } + } + #endregion } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index dc98c5b813..f182bda7b6 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -64,6 +64,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 549c0844ff..e527724add 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Website.Routing; @@ -39,7 +40,7 @@ public static class UmbracoApplicationBuilderExtensions FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService(); surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder); - builder.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); + builder.EndpointRouteBuilder.MapDynamicControllerRoute(Constants.Web.Routing.DynamicRoutePattern); return builder; } diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs new file mode 100644 index 0000000000..4098b21928 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Website.Routing; + + +/** + * A matcher policy that discards the catch-all (slug) route if there are any other valid routes with a lower order. + * + * The purpose of this is to skip our expensive if it's not required, + * for instance if there's a statically routed endpoint registered before the dynamic route, + * for more information see: https://github.com/umbraco/Umbraco-CMS/issues/16015. + * The core reason why this is necessary is that ALL routes get evaluated: + * " + * all routes get evaluated, they get to produce candidates and then the best candidate is selected. + * Since you have a dynamic route, it needs to run to produce the final endpoints and + * then those are ranked in along with the rest of the candidates to choose the final endpoint. + * " + * From: https://github.com/dotnet/aspnetcore/issues/45175#issuecomment-1322497958 + * + * This also handles rerouting under install/upgrade states. + */ + +internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy +{ + private readonly IRuntimeState _runtimeState; + private readonly EndpointDataSource _endpointDataSource; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + private GlobalSettings _globalSettings; + private readonly Lazy _installEndpoint; + private readonly Lazy _renderEndpoint; + + public EagerMatcherPolicy( + IRuntimeState runtimeState, + EndpointDataSource endpointDataSource, + UmbracoRequestPaths umbracoRequestPaths, + IOptionsMonitor globalSettings) + { + _runtimeState = runtimeState; + _endpointDataSource = endpointDataSource; + _umbracoRequestPaths = umbracoRequestPaths; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(settings => _globalSettings = settings); + _installEndpoint = new Lazy(GetInstallEndpoint); + _renderEndpoint = new Lazy(GetRenderEndpoint); + } + + // We want this to run as the very first policy, so we can discard the UmbracoRouteValueTransformer before the framework runs it. + public override int Order => int.MinValue + 10; + + // We know we don't have to run this matcher against the backoffice endpoints. + public bool AppliesToEndpoints(IReadOnlyList endpoints) => true; + + public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (_runtimeState.Level != RuntimeLevel.Run) + { + var handled = await HandleInstallUpgrade(httpContext, candidates); + if (handled) + { + return; + } + } + + // If there's only one candidate, we don't need to do anything. + if (candidates.Count < 2) + { + return; + } + + // If there are multiple candidates, we want to discard the catch-all (slug) + // IF there is any candidates with a lower order. Since this will be a statically routed endpoint registered before the dynamic route. + // Which means that we don't have to run our UmbracoRouteValueTransformer to route dynamically (expensive). + var lowestOrder = int.MaxValue; + int? dynamicId = null; + RouteEndpoint? dynamicEndpoint = null; + for (var i = 0; i < candidates.Count; i++) + { + CandidateState candidate = candidates[i]; + + // If it's not a RouteEndpoint there's not much we can do to count it in the order. + if (candidate.Endpoint is not RouteEndpoint routeEndpoint) + { + continue; + } + + if (routeEndpoint.Order < lowestOrder) + { + // We have to ensure that the route is valid for the current request method. + // This is because attribute routing will always have an order of 0. + // This means that you could attribute route a POST to /example, but also have an umbraco page at /example + // This would then result in a 404, because we'd see the attribute route with order 0, and always consider that the lowest order + // We'd then disable the dynamic endpoint since another endpoint has a lower order, and end up with only 1 invalid endpoint. + // (IsValidCandidate does not take this into account since the candidate itself is still valid) + HttpMethodMetadata? methodMetaData = routeEndpoint.Metadata.GetMetadata(); + if (methodMetaData?.HttpMethods.Contains(httpContext.Request.Method) is false) + { + continue; + } + + lowestOrder = routeEndpoint.Order; + } + + // We only want to consider our dynamic route, this way it's still possible to register your own custom route before ours. + if (routeEndpoint.DisplayName != Constants.Web.Routing.DynamicRoutePattern) + { + continue; + } + + dynamicEndpoint = routeEndpoint; + dynamicId = i; + } + + // Invalidate the dynamic route if another route has a lower order. + // This means that if you register your static route after the dynamic route, the dynamic route will take precedence + // This more closely resembles the existing behaviour. + if (dynamicEndpoint is not null && dynamicId is not null && dynamicEndpoint.Order > lowestOrder) + { + candidates.SetValidity(dynamicId.Value, false); + } + } + + /// + /// Replaces the first endpoint candidate with the specified endpoint, invalidating all other candidates, + /// guaranteeing that the specified endpoint will be hit. + /// + /// The candidate set to manipulate. + /// The target endpoint that will be hit. + /// + private static void SetEndpoint(CandidateSet candidates, Endpoint endpoint, RouteValueDictionary routeValueDictionary) + { + candidates.ReplaceEndpoint(0, endpoint, routeValueDictionary); + + for (int i = 1; i < candidates.Count; i++) + { + candidates.SetValidity(1, false); + } + } + + private Endpoint GetInstallEndpoint() + { + Endpoint endpoint = _endpointDataSource.Endpoints.First(x => + { + ControllerActionDescriptor? descriptor = x.Metadata.GetMetadata(); + return descriptor?.ControllerTypeInfo.Name == "BackOfficeDefaultController" + && descriptor.ActionName == "Index"; + }); + + return endpoint; + } + + private Endpoint GetRenderEndpoint() + { + Endpoint endpoint = _endpointDataSource.Endpoints.First(x => + { + ControllerActionDescriptor? descriptor = x.Metadata.GetMetadata(); + return descriptor?.ControllerTypeInfo == typeof(RenderController) + && descriptor.ActionName == nameof(RenderController.Index); + }); + + return endpoint; + } + + private Task HandleInstallUpgrade(HttpContext httpContext, CandidateSet candidates) + { + if (_runtimeState.Level != RuntimeLevel.Upgrade) + { + // We need to let the installer API requests through + // Currently we do this with a check for the installer path + // Ideally we should do this in a more robust way, for instance with a dedicated attribute we can then check for. + if (_umbracoRequestPaths.IsInstallerRequest(httpContext.Request.Path)) + { + return Task.FromResult(true); + } + + SetEndpoint(candidates, _installEndpoint.Value, new RouteValueDictionary + { + [Constants.Web.Routing.ControllerToken] = "BackOfficeDefault", + [Constants.Web.Routing.ActionToken] = "Index", + }); + + return Task.FromResult(true); + } + + // Check if maintenance page should be shown + // Current behaviour is that statically routed endpoints still work in upgrade state + // This means that IF there is a static route, we should not show the maintenance page. + // And instead carry on as we normally would. + var hasStaticRoute = false; + for (var i = 0; i < candidates.Count; i++) + { + CandidateState candidate = candidates[i]; + IDynamicEndpointMetadata? dynamicEndpointMetadata = candidate.Endpoint.Metadata.GetMetadata(); + if (dynamicEndpointMetadata is null || dynamicEndpointMetadata.IsDynamic is false) + { + hasStaticRoute = true; + break; + } + } + + if (_runtimeState.Level != RuntimeLevel.Upgrade + || _globalSettings.ShowMaintenancePageWhenInUpgradeState is false + || hasStaticRoute) + { + return Task.FromResult(false); + } + + // Otherwise we'll re-route to the render controller (this will in turn show the maintenance page through a filter) + // With this approach however this could really just be a plain old endpoint instead of a filter. + SetEndpoint(candidates, _renderEndpoint.Value, new RouteValueDictionary + { + [Constants.Web.Routing.ControllerToken] = ControllerExtensions.GetControllerName(), + [Constants.Web.Routing.ActionToken] = nameof(RenderController.Index), + }); + + return Task.FromResult(true); + + } +} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 790d99952a..6754577968 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -7,12 +7,10 @@ using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Website.Controllers; @@ -85,22 +83,6 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer public override async ValueTask TransformAsync( HttpContext httpContext, RouteValueDictionary values) { - // If we aren't running, then we have nothing to route. We allow the frontend to continue while in upgrade mode. - if (_runtime.Level != RuntimeLevel.Run && _runtime.Level != RuntimeLevel.Upgrade) - { - if (_runtime.Level == RuntimeLevel.Install && !httpContext.Request.IsClientSideRequest()) - { - return new RouteValueDictionary() - { - //TODO figure out constants - [ControllerToken] = "BackOfficeDefault", - [ActionToken] = "Index" - }; - } - - return null!; - } - // will be null for any client side requests like JS, etc... if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { @@ -117,17 +99,6 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer return null!; } - // Check if the maintenance page should be shown - if (_runtime.Level == RuntimeLevel.Upgrade && _globalSettings.ShowMaintenancePageWhenInUpgradeState) - { - return new RouteValueDictionary - { - // Redirects to the RenderController who handles maintenance page in a filter, instead of having a dedicated controller - [ControllerToken] = ControllerExtensions.GetControllerName(), - [ActionToken] = nameof(RenderController.Index), - }; - } - // Check if there is no existing content and return the no content controller if (!umbracoContext.Content?.HasContent() ?? false) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 0ce5e013f3..20dd4ecb46 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -57,6 +57,11 @@ public class TelemetryServiceTests : UmbracoIntegrationTest Constants.Telemetry.WebhookTotal, Constants.Telemetry.WebhookCustomHeaders, Constants.Telemetry.WebhookCustomEvent, + Constants.Telemetry.RichTextEditorCount, + Constants.Telemetry.RichTextBlockCount, + Constants.Telemetry.TotalPropertyCount, + Constants.Telemetry.HighestPropertyCount, + Constants.Telemetry.TotalCompositions, }; // Add the default webhook events. diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs index 7d117b96c5..a4a15b8f22 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PublishedContentVarianceTests.cs @@ -76,6 +76,28 @@ public class PublishedContentVarianceTests Assert.AreEqual(expectedValue, value); } + [TestCase(DaCulture, Segment1, "DaDk property value")] + [TestCase(DaCulture, Segment2, "DaDk property value")] + [TestCase(EnCulture, Segment1, "EnUs property value")] + [TestCase(EnCulture, Segment2, "EnUs property value")] + public void Content_Culture_And_Segment_Variation_Can_Get_Culture_Variant_Property(string culture, string segment, string expectedValue) + { + var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Culture, variationContextCulture: culture, variationContextSegment: segment); + var value = GetPropertyValue(content); + Assert.AreEqual(expectedValue, value); + } + + [TestCase(DaCulture, Segment1, "Segment1 property value")] + [TestCase(DaCulture, Segment2, "Segment2 property value")] + [TestCase(EnCulture, Segment1, "Segment1 property value")] + [TestCase(EnCulture, Segment2, "Segment2 property value")] + public void Content_Culture_And_Segment_Variation_Can_Get_Segment_Variant_Property(string culture, string segment, string expectedValue) + { + var content = CreatePublishedContent(ContentVariation.CultureAndSegment, ContentVariation.Segment, variationContextCulture: culture, variationContextSegment: segment); + var value = GetPropertyValue(content); + Assert.AreEqual(expectedValue, value); + } + private object? GetPropertyValue(IPublishedContent content) => content.GetProperty(PropertyTypeAlias)!.GetValue(); private IPublishedContent CreatePublishedContent(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, string? variationContextCulture = null, string? variationContextSegment = null) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs index 16769d42b8..3d17117924 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs @@ -97,23 +97,6 @@ public class UmbracoRequestPathsTests Assert.AreEqual(expected, umbracoRequestPaths.IsBackOfficeRequest(source.AbsolutePath)); } - [TestCase("http://www.domain.com/install", true)] - [TestCase("http://www.domain.com/Install/", true)] - [TestCase("http://www.domain.com/install/default.aspx", true)] - [TestCase("http://www.domain.com/install/test/test", true)] - [TestCase("http://www.domain.com/Install/test/test.aspx", true)] - [TestCase("http://www.domain.com/install/test/test.js", true)] - [TestCase("http://www.domain.com/instal", false)] - [TestCase("http://www.domain.com/umbraco", false)] - [TestCase("http://www.domain.com/umbraco/umbraco", false)] - public void Is_Installer_Request(string input, bool expected) - { - var source = new Uri(input); - var hostingEnvironment = CreateHostingEnvironment(); - var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment, Options.Create(_umbracoRequestPathsOptions)); - Assert.AreEqual(expected, umbracoRequestPaths.IsInstallerRequest(source.AbsolutePath)); - } - [TestCase("http://www.domain.com/some/path", false)] [TestCase("http://www.domain.com/umbraco/surface/blah", false)] [TestCase("http://www.domain.com/umbraco/api/blah", false)] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs index 4072e3df8b..72ab9150bc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs @@ -30,4 +30,87 @@ public class WebPathTests [Test] public void Combine_must_handle_null() => Assert.Throws(() => WebPath.Combine(null)); + + + [Test] + [TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)] + [TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)] + [TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)] + [TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)] + [TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)] + [TestCase("/test", UriKind.Absolute, ExpectedResult = false)] + [TestCase("test", UriKind.Absolute, ExpectedResult = false)] + [TestCase("", UriKind.Absolute, ExpectedResult = false)] + [TestCase(null, UriKind.Absolute, ExpectedResult = false)] + [TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)] + [TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)] + [TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)] + [TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)] + [TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)] + [TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)] + [TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)] + [TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)] + [TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)] + [TestCase("/test", UriKind.Relative, ExpectedResult = true)] + [TestCase("test", UriKind.Relative, ExpectedResult = true)] + [TestCase("", UriKind.Relative, ExpectedResult = false)] + [TestCase(null, UriKind.Relative, ExpectedResult = false)] + [TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)] + [TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)] + [TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)] + [TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)] + [TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)] + public bool IsWellFormedWebPath(string? webPath, UriKind uriKind) => WebPath.IsWellFormedWebPath(webPath, uriKind); + }