From 52c21b0fcaf5b1b2c7c55d7e9d9d61638eceb1e3 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 20 Mar 2024 13:20:40 +0100 Subject: [PATCH 01/61] Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) --- src/JsonSchema/JsonSchema.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 9ef072ccdf..b5af8ae7c1 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -13,7 +13,7 @@ - - + + From 6379f2fd35f1b3e0d7c87a94eefd7bd59891324c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Apr 2024 08:55:40 +0200 Subject: [PATCH 02/61] 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 --- .../Property.cs | 185 +++++++++--------- 1 file changed, 91 insertions(+), 94 deletions(-) diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index 2892a04f90..596bae2090 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,7 +20,6 @@ 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 @@ -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, @@ -125,30 +123,27 @@ 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) @@ -160,19 +155,16 @@ internal class Property : PublishedPropertyBase 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 +219,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 +233,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 +260,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,22 +282,19 @@ internal class Property : PublishedPropertyBase { _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); - lock (_locko) + CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (cacheValues.XPathInitialized) { - CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); - - // initial reference cache level always is .Content - const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - - if (cacheValues.XPathInitialized) - { - return cacheValues.XPathValue; - } - - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); - cacheValues.XPathInitialized = true; return cacheValues.XPathValue; } + + cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); + cacheValues.XPathInitialized = true; + return cacheValues.XPathValue; } public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null) @@ -321,18 +302,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; } @@ -382,9 +361,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) @@ -394,14 +373,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; } @@ -431,5 +411,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 } From a6a76d1815f30f5bae569f42076a95ef5d6aa6c4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Apr 2024 12:03:44 +0200 Subject: [PATCH 03/61] Make the API content response builder extendable (#16056) * Make the API content response builder extendable * DeliveryApiJsonTypeResolver needs to be extendable too --- .../Json/DeliveryApiJsonTypeResolver.cs | 35 +++++++++++++------ .../DeliveryApi/ApiContentResponseBuilder.cs | 11 ++++-- 2 files changed, 32 insertions(+), 14 deletions(-) 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/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 Date: Thu, 18 Apr 2024 09:10:22 +0200 Subject: [PATCH 04/61] bump rc to regular --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index f70819e8b4..6ab0f37af9 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.3.0-rc", + "version": "13.3.0", "assemblyVersion": { "precision": "build" }, From a27a4dcd84f84910888578d66f605b52247db2e6 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 18 Apr 2024 15:36:44 +0200 Subject: [PATCH 05/61] Bump to next minor --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 6ab0f37af9..b9fa941e03 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.3.0", + "version": "13.4.0", "assemblyVersion": { "precision": "build" }, From 09803501722d17b34f7d991f43cb5479655e22f8 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Apr 2024 12:56:36 +0200 Subject: [PATCH 06/61] 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 --- src/Umbraco.Core/Constants-Telemetry.cs | 5 +++ .../EmbeddedResources/Lang/en.xml | 2 +- .../EmbeddedResources/Lang/en_us.xml | 2 +- .../UmbracoBuilder.TelemetryProviders.cs | 1 + .../BlocksInRichTextTelemetryProvider.cs | 42 +++++++++++++++++++ .../PropertyEditorTelemetryProvider.cs | 8 ++++ .../Telemetry/TelemetryServiceTests.cs | 5 +++ 7 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index b5c1e15c94..4ea1182226 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -36,5 +36,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/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 5559768c67..4995967993 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -3077,7 +3077,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
  • 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 35dfc458ce..39b20009f4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -3096,7 +3096,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont 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, 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.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index 69ee653c19..e18a8fbdf8 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -21,6 +21,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/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/BlocksInRichTextTelemetryProvider.cs new file mode 100644 index 0000000000..2af4a7f6ad --- /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.GetByEditorAlias(Constants.PropertyEditors.Aliases.TinyMce).ToArray(); + int registeredBlocks = 0; + yield return new UsageInformation(Constants.Telemetry.RichTextEditorCount, richTextDataTypes.Count()); + + foreach (IDataType richTextDataType in richTextDataTypes) + { + if (richTextDataType.Configuration 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/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index e5691c15cb..67b2f63c19 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -58,6 +58,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. From 119fde2033e7c72d7d5e16cbc7a5539ec38fb0d0 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:44:20 +0200 Subject: [PATCH 07/61] 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 --- .../Controllers/MediaController.cs | 147 ++++++++++-------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 807061e5aa..c81cd9cdb4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -189,7 +189,7 @@ public class MediaController : ContentControllerBase if (mapped is not null) { - //remove the listview app if it exists + // remove the listview app if it exists mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); } @@ -205,7 +205,7 @@ public class MediaController : ContentControllerBase var apps = new List { ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "media", - Constants.DataTypes.DefaultMediaListView) + Constants.DataTypes.DefaultMediaListView) }; apps[0].Active = true; var display = new MediaItemDisplay @@ -238,7 +238,8 @@ public class MediaController : ContentControllerBase if (foundMedia == null) { HandleContentNotFound(id); - //HandleContentNotFound will throw an exception + + // HandleContentNotFound will throw an exception return null; } @@ -306,8 +307,8 @@ public class MediaController : ContentControllerBase public PagedResult> GetChildFolders(int id, int pageNumber = 1, int pageSize = 1000) { - //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... - //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" + // Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... + // if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" var folderTypes = _mediaTypeService .GetAll() .Where(x => x.Alias.EndsWith("Folder")) @@ -320,7 +321,8 @@ public class MediaController : ContentControllerBase } IEnumerable children = _mediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out long total, - //lookup these content types + + // lookup these content types _sqlContext.Query().Where(x => folderTypes.Contains(x.ContentTypeId)), Ordering.By("Name")); @@ -336,6 +338,7 @@ public class MediaController : ContentControllerBase /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] public IEnumerable> GetRootMedia() => + // TODO: Add permissions check! _mediaService.GetRootMedia()? .Select(_umbracoMapper.Map>).WhereNotNull() ?? @@ -357,7 +360,7 @@ public class MediaController : ContentControllerBase return HandleContentNotFound(id); } - //if the current item is in the recycle bin + // if the current item is in the recycle bin if (foundMedia.Trashed == false) { Attempt moveResult = _mediaService.MoveToRecycleBin(foundMedia, @@ -389,8 +392,10 @@ public class MediaController : ContentControllerBase { // Authorize... var requirement = new MediaPermissionsResourceRequirement(); - AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(User, - new MediaPermissionsResource(_mediaService.GetById(move.Id)), requirement); + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync( + User, + new MediaPermissionsResource(_mediaService.GetById(move.Id)), + requirement); if (!authorizationResult.Succeeded) { return Forbid(); @@ -403,18 +408,20 @@ public class MediaController : ContentControllerBase return convertToActionResult.Convert(); } - var destinationParentID = move.ParentId; - var sourceParentID = toMove?.ParentId; + var destinationParentId = move.ParentId; + var sourceParentId = toMove?.ParentId; var moveResult = toMove is null ? false : _mediaService.Move(toMove, move.ParentId, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); - if (sourceParentID == destinationParentID) + if (sourceParentId == destinationParentId) { - return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("", - _localizedTextService.Localize("media", "moveToSameFolderFailed"), NotificationStyle.Error))); + return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification( + string.Empty, + _localizedTextService.Localize("media", "moveToSameFolderFailed"), + NotificationStyle.Error))); } if (moveResult == false) @@ -435,9 +442,9 @@ public class MediaController : ContentControllerBase public ActionResult? PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { - //Recent versions of IE/Edge may send in the full client side file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). + // Recent versions of IE/Edge may send in the full client side file path instead of just the file name. + // To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + // uploaded files to being *only* the actual file name (as it should be). if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) { foreach (ContentPropertyFile file in contentItem.UploadedFiles) @@ -446,14 +453,14 @@ public class MediaController : ContentControllerBase } } - //If we've reached here it means: + // If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - //Don't update the name if it is empty + // Don't update the name if it is empty if (contentItem.Name.IsNullOrWhiteSpace() == false && contentItem.PersistedContent is not null) { contentItem.PersistedContent.Name = contentItem.Name; @@ -466,14 +473,14 @@ public class MediaController : ContentControllerBase (save, property, v) => property?.SetValue(v), //set prop val null); // media are all invariant - //we will continue to save if model state is invalid, however we cannot save if critical data is missing. - //TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live' + // we will continue to save if model state is invalid, however we cannot save if critical data is missing. + // TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live' if (!ModelState.IsValid) { - //check for critical data validation issues, we can't continue saving if this data is invalid + // check for critical data validation issues, we can't continue saving if this data is invalid if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem)) { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw validation response MediaItemDisplay? forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); return ValidationProblem(forDisplay, ModelState); @@ -485,20 +492,20 @@ public class MediaController : ContentControllerBase return null; } - //save the item + // save the item Attempt saveStatus = _mediaService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); - //return the updated model + // return the updated model MediaItemDisplay? display = _umbracoMapper.Map(contentItem.PersistedContent); - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden); } - //put the correct msgs in + // put the correct msgs in switch (contentItem.Action) { case ContentSaveAction.Save: @@ -513,7 +520,7 @@ public class MediaController : ContentControllerBase { AddCancelMessage(display); - //If the item is new and the operation was cancelled, we need to return a different + // If the item is new and the operation was cancelled, we need to return a different // status code so the UI can handle it since it won't be able to redirect since there // is no Id to redirect to! if (saveStatus.Result?.Result == OperationResultType.FailedCancelledByEvent && @@ -554,7 +561,7 @@ public class MediaController : ContentControllerBase return NotFound(); } - //if there's nothing to sort just return ok + // if there's nothing to sort just return ok if (sorted.IdSortOrder?.Length == 0) { return Ok(); @@ -595,7 +602,7 @@ public class MediaController : ContentControllerBase public async Task> PostAddFolder(PostedFolder folder) { ActionResult? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true); - if (!(parentIdResult?.Result is null)) + if (parentIdResult?.Result is not null) { return new ActionResult(parentIdResult.Result); } @@ -632,15 +639,15 @@ public class MediaController : ContentControllerBase //ensure it exists Directory.CreateDirectory(root); - //must have a file + // must have a file if (file is null || file.Count == 0) { return NotFound("No file was uploaded"); } - //get the string json from the request + // get the string json from the request ActionResult? parentIdResult = await GetParentIdAsIntAsync(currentFolder, true); - if (!(parentIdResult?.Result is null)) + if (parentIdResult?.Result is not null) { return parentIdResult.Result; } @@ -653,7 +660,7 @@ public class MediaController : ContentControllerBase var tempFiles = new PostedFiles(); - //in case we pass a path with a folder in it, we will create it and upload media to it. + // in case we pass a path with a folder in it, we will create it and upload media to it. if (!string.IsNullOrEmpty(path)) { if (!IsFolderCreationAllowedHere(parentId.Value)) @@ -669,16 +676,16 @@ public class MediaController : ContentControllerBase var folderName = folders[i]; IMedia? folderMediaItem; - //if uploading directly to media root and not a subfolder + // if uploading directly to media root and not a subfolder if (parentId == Constants.System.Root) { - //look for matching folder + // look for matching folder folderMediaItem = _mediaService.GetRootMedia()?.FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); if (folderMediaItem == null) { - //if null, create a folder + // if null, create a folder folderMediaItem = _mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); _mediaService.Save(folderMediaItem); @@ -686,10 +693,10 @@ public class MediaController : ContentControllerBase } else { - //get current parent + // get current parent IMedia? mediaRoot = _mediaService.GetById(parentId.Value); - //if the media root is null, something went wrong, we'll abort + // if the media root is null, something went wrong, we'll abort if (mediaRoot == null) { return Problem( @@ -697,7 +704,7 @@ public class MediaController : ContentControllerBase " returned null"); } - //look for matching folder + // look for matching folder folderMediaItem = FindInChildren(mediaRoot.Id, folderName, Constants.Conventions.MediaTypes.Folder); if (folderMediaItem == null) @@ -709,7 +716,7 @@ public class MediaController : ContentControllerBase } } - //set the media root to the folder id so uploaded files will end there. + // set the media root to the folder id so uploaded files will end there. parentId = folderMediaItem.Id; } } @@ -749,7 +756,7 @@ public class MediaController : ContentControllerBase } } - //Only set the permission-based mediaType if we only allow 1 specific file under this parent. + // Only set the permission-based mediaType if we only allow 1 specific file under this parent. if (allowedContentTypes.Count == 1 && mediaTypeItem != null) { mediaTypeAlias = mediaTypeItem.Alias; @@ -762,7 +769,7 @@ public class MediaController : ContentControllerBase allowedContentTypes.UnionWith(typesAllowedAtRoot); } - //get the files + // get the files foreach (IFormFile formFile in file) { var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd(); @@ -821,6 +828,11 @@ public class MediaController : ContentControllerBase continue; } + if (allowedContentTypes.Any(x => x.Alias == mediaTypeItem.Alias) == false) + { + continue; + } + mediaTypeAlias = mediaTypeItem.Alias; break; } @@ -866,8 +878,8 @@ public class MediaController : ContentControllerBase IMedia createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); - createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, - _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream); + createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, + _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream); Attempt saveResult = _mediaService.Save(createdMediaItem, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); @@ -878,13 +890,13 @@ public class MediaController : ContentControllerBase } } - //Different response if this is a 'blueimp' request + // Different response if this is a 'blueimp' request if (HttpContext.Request.Query.Any(x => x.Key == "origin")) { KeyValuePair origin = HttpContext.Request.Query.First(x => x.Key == "origin"); if (origin.Value == "blueimp") { - return new JsonResult(tempFiles); //Don't output the angular xsrf stuff, blue imp doesn't like that + return new JsonResult(tempFiles); // Don't output the angular xsrf stuff, blue imp doesn't like that } } @@ -923,7 +935,11 @@ public class MediaController : ContentControllerBase var total = long.MaxValue; while (page * pageSize < total) { - IEnumerable children = _mediaService.GetPagedChildren(mediaId, page++, pageSize, out total, + IEnumerable children = _mediaService.GetPagedChildren( + mediaId, + page++, + pageSize, + out total, _sqlContext.Query().Where(x => x.Name == nameToFind)); IMedia? match = children.FirstOrDefault(c => c.ContentType.Alias == contentTypeAlias); if (match != null) @@ -946,14 +962,13 @@ public class MediaController : ContentControllerBase /// private async Task?> GetParentIdAsIntAsync(string? parentId, bool validatePermissions) { - // test for udi if (UdiParser.TryParse(parentId, out GuidUdi? parentUdi)) { parentId = parentUdi?.Guid.ToString(); } - //if it's not an INT then we'll check for GUID + // if it's not an INT then we'll check for GUID if (int.TryParse(parentId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intParentId) == false) { // if a guid then try to look up the entity @@ -977,7 +992,7 @@ public class MediaController : ContentControllerBase } // Authorize... - //ensure the user has access to this folder by parent id! + // ensure the user has access to this folder by parent id! if (validatePermissions) { var requirement = new MediaPermissionsResourceRequirement(); @@ -1018,14 +1033,14 @@ public class MediaController : ContentControllerBase if (model.ParentId < 0) { - //cannot move if the content item is not allowed at the root unless there are - //none allowed at root (in which case all should be allowed at root) + // cannot move if the content item is not allowed at the root unless there are + // none allowed at root (in which case all should be allowed at root) IMediaTypeService mediaTypeService = _mediaTypeService; if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"), - ""); + string.Empty); return ValidationProblem(notificationModel); } } @@ -1037,7 +1052,7 @@ public class MediaController : ContentControllerBase return NotFound(); } - //check if the item is allowed under this one + // check if the item is allowed under this one IMediaType? parentContentType = _mediaTypeService.Get(parent.ContentTypeId); if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) @@ -1049,12 +1064,12 @@ public class MediaController : ContentControllerBase } // Check on paths - if (string.Format(",{0},", parent.Path) - .IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + if ($",{parent.Path}," + .IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1) { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"), - ""); + string.Empty); return ValidationProblem(notificationModel); } } @@ -1110,7 +1125,8 @@ public class MediaController : ContentControllerBase /// Returns the child media objects - using the entity INT id /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(int id, + public PagedResult> GetChildren( + int id, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -1118,7 +1134,7 @@ public class MediaController : ContentControllerBase bool orderBySystemField = true, string filter = "") { - //if a request is made for the root node data but the user's start node is not the default, then + // if a request is made for the root node data but the user's start node is not the default, then // we need to return their start nodes if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) @@ -1148,7 +1164,6 @@ public class MediaController : ContentControllerBase } // else proceed as usual - long totalChildren; List children; if (pageNumber > 0 && pageSize > 0) @@ -1156,7 +1171,7 @@ public class MediaController : ContentControllerBase IQuery? queryFilter = null; if (filter.IsNullOrWhiteSpace() == false) { - //add the default text filter + // add the default text filter queryFilter = _sqlContext.Query() .Where(x => x.Name != null) .Where(x => x.Name!.Contains(filter)); @@ -1164,14 +1179,16 @@ public class MediaController : ContentControllerBase children = _mediaService .GetPagedChildren( - id, pageNumber - 1, pageSize, + id, + pageNumber - 1, + pageSize, out totalChildren, queryFilter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)).ToList(); } else { - //better to not use this without paging where possible, currently only the sort dialog does + // better to not use this without paging where possible, currently only the sort dialog does children = _mediaService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList(); totalChildren = children.Count; } @@ -1184,7 +1201,7 @@ public class MediaController : ContentControllerBase var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize) { Items = children - .Select(_umbracoMapper.Map>).WhereNotNull() + .Select(_umbracoMapper.Map>).WhereNotNull() }; return pagedResult; From cae106bfe8fa11c080fc90ba4354a0791e47f9a2 Mon Sep 17 00:00:00 2001 From: Joshua Daniel Pratt Nielsen Date: Sat, 20 Apr 2024 01:32:55 +0200 Subject: [PATCH 08/61] Fix logic for retrieving lastKnownElement --- .../forms/umbfocuslock.directive.js | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 69c11a11cc..3d412d34e1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -88,26 +88,27 @@ // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ - // If there is only one editor open, search for the "editor-info" inside it and set focus on it // This is relevant when a property editor has been selected and the editor where we selected it from // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array - if(infiniteEditors && infiniteEditors.length === 1){ - var editorInfo = infiniteEditors[0].querySelector('.editor-info'); - if(infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { - lastKnownElement = editorInfo; - // Clear the array - clearLastKnownFocusedElements(); - } + var editorInfo = (infiniteEditors && infiniteEditors.length === 1) + ? infiniteEditors[0].querySelector('.editor-info') + : null; + + if(editorInfo !== null){ + lastKnownElement = editorInfo; + + // Clear the array + clearLastKnownFocusedElements(); } - else { - var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; - lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; + else{ + var lastIndex = $rootScope.lastKnownFocusableElements.length - 1; + lastKnownElement = $rootScope.lastKnownFocusableElements[lastIndex]; - // Remove the last item from the array so we always set the correct lastKnowFocus for each layer - $rootScope.lastKnownFocusableElements.splice(lastItemIndex, 1); + // Remove the last item from the array so we always set the correct lastKnowFocus for each layer + $rootScope.lastKnownFocusableElements.splice(lastIndex, 1); } // Update the lastknowelement variable here From 599ec18ecc6b2890d96654c19a516f704aef1cf6 Mon Sep 17 00:00:00 2001 From: Lars-Erik Date: Thu, 4 Apr 2024 13:44:20 +0200 Subject: [PATCH 09/61] Implementors using Umbraco.Tests.Integration won't have to override GetLocalizedTextService (cherry picked from commit b0016687eb583a549da8992f5fba92e269b4cbfa) (cherry picked from commit 2bb56f1b81a84df3ef03ba0170f741cea6007c28) --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index dac260b985..bae9c7efbe 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -123,7 +123,18 @@ public static class UmbracoBuilderExtensions var currFolder = new DirectoryInfo(srcFolder); - var uiProject = currFolder.GetDirectories("Umbraco.Web.UI", SearchOption.TopDirectoryOnly).First(); + if (!currFolder.Exists) + { + currFolder = new DirectoryInfo(Path.GetTempPath()); + } + + var uiProject = currFolder.GetDirectories("Umbraco.Web.UI", SearchOption.TopDirectoryOnly).FirstOrDefault(); + if (uiProject == null) + { + uiProject = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "Umbraco.Web.UI")); + uiProject.Create(); + } + var mainLangFolder = new DirectoryInfo(Path.Combine(uiProject.FullName, globalSettings.Value.UmbracoPath.TrimStart("~/"), "config", "lang")); return new LocalizedTextServiceFileSources( From 5b46c718e69e902ff7f82a68bd780d51cd2c0caf Mon Sep 17 00:00:00 2001 From: Joshua Daniel Pratt Nielsen Date: Sat, 20 Apr 2024 01:32:55 +0200 Subject: [PATCH 10/61] Fix logic for retrieving lastKnownElement (cherry picked from commit cae106bfe8fa11c080fc90ba4354a0791e47f9a2) --- .../forms/umbfocuslock.directive.js | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 69c11a11cc..3d412d34e1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -88,26 +88,27 @@ // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ - // If there is only one editor open, search for the "editor-info" inside it and set focus on it // This is relevant when a property editor has been selected and the editor where we selected it from // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array - if(infiniteEditors && infiniteEditors.length === 1){ - var editorInfo = infiniteEditors[0].querySelector('.editor-info'); - if(infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { - lastKnownElement = editorInfo; - // Clear the array - clearLastKnownFocusedElements(); - } + var editorInfo = (infiniteEditors && infiniteEditors.length === 1) + ? infiniteEditors[0].querySelector('.editor-info') + : null; + + if(editorInfo !== null){ + lastKnownElement = editorInfo; + + // Clear the array + clearLastKnownFocusedElements(); } - else { - var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; - lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; + else{ + var lastIndex = $rootScope.lastKnownFocusableElements.length - 1; + lastKnownElement = $rootScope.lastKnownFocusableElements[lastIndex]; - // Remove the last item from the array so we always set the correct lastKnowFocus for each layer - $rootScope.lastKnownFocusableElements.splice(lastItemIndex, 1); + // Remove the last item from the array so we always set the correct lastKnowFocus for each layer + $rootScope.lastKnownFocusableElements.splice(lastIndex, 1); } // Update the lastknowelement variable here From b6031dea1a86f15e0271d711aa14806ec5b1758a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 3 May 2024 09:27:20 +0200 Subject: [PATCH 11/61] bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 6ab0f37af9..fcf4a18f98 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.3.0", + "version": "13.3.1", "assemblyVersion": { "precision": "build" }, From edb516f71ae323a34f4268c7628b769f1ade627b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 3 May 2024 09:28:43 +0200 Subject: [PATCH 12/61] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 20b8b28182..500ce474e2 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.3.9", + "version": "12.3.10", "assemblyVersion": { "precision": "build" }, From fee222daff2321b3f8070945ec5de41953fa3a53 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 3 May 2024 09:29:37 +0200 Subject: [PATCH 13/61] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 547407d05f..f49d971518 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.8.5", + "version": "10.8.6", "assemblyVersion": { "precision": "build" }, From 23d0a6b9b2a237008c005cb17e2552e23651de4c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 6 May 2024 09:40:52 +0200 Subject: [PATCH 14/61] Since v13 properties can sometimes be of type IRichTextEditorIntermediateValue - this was unexpected in the XPath navigator code (#16121) --- src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs index 020f753165..b21666b2b6 100644 --- a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.XPath; +using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Xml.XPath; @@ -641,6 +642,11 @@ public class NavigableNavigator : XPathNavigator return true; } + if (valueForXPath is IRichTextEditorIntermediateValue) + { + return false; + } + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); } From cfcdc9c98d94c61271ff07c769e8ff23ec3089f6 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 6 May 2024 13:14:11 +0200 Subject: [PATCH 15/61] 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 --- src/Umbraco.Core/Models/WebhookLog.cs | 2 ++ .../Services/WebhookLogFactory.cs | 5 ++-- .../Factories/WebhookLogFactory.cs | 4 ++- .../Mapping/WebhookMapDefinition.cs | 1 + .../Models/WebhookLogViewModel.cs | 3 +++ .../src/views/webhooks/logs.controller.js | 26 +++++++++++++++---- .../src/views/webhooks/logs.html | 23 +++++++++++----- .../webhooks/overlays/details.controller.js | 18 ++++++++++++- .../src/views/webhooks/overlays/details.html | 14 +++++----- .../src/views/webhooks/overview.controller.js | 10 +++---- 10 files changed, 80 insertions(+), 26 deletions(-) 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/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/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.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index 1d1558026c..017004e266 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -67,6 +67,7 @@ public class WebhookMapDefinition : IMapDefinition target.Url = source.Url; target.RequestHeaders = source.RequestHeaders; target.WebhookKey = source.WebhookKey; + target.IsSuccessStatusCode = source.IsSuccessStatusCode; if (_hostingEnvironment.IsDebugMode) { diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs index 0b3ff552a7..4518c77637 100644 --- a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs +++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs @@ -14,6 +14,9 @@ public class WebhookLogViewModel [DataMember(Name = "statusCode")] public string StatusCode { get; set; } = string.Empty; + [DataMember(Name = "isSuccessStatusCode")] + public bool IsSuccessStatusCode { get; set; } + [DataMember(Name = "date")] public DateTime Date { get; set; } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js index c4d032b806..5afa4fd00d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js @@ -5,9 +5,13 @@ const vm = this; + vm.pagination = { + pageNumber: 1, + pageSize: 25 + }; + vm.logs = []; vm.openLogOverlay = openLogOverlay; - vm.isChecked = isChecked; function init() { vm.loading = true; @@ -22,9 +26,14 @@ } function loadLogs() { - return webhooksResource.getLogs() + const take = vm.pagination.pageSize; + const skip = (vm.pagination.pageNumber - 1) * take; + + return webhooksResource.getLogs(skip, take) .then(data => { vm.logs = data.items; + vm.pagination.totalPages = Math.ceil(data.totalItems/vm.pagination.pageSize); + vm.logs.forEach(log => { formatDatesToLocal(log); }); @@ -54,9 +63,16 @@ editorService.open(dialog); } - function isChecked(log) { - return log.statusCode === "OK (200)"; - } + vm.previousPage = () => vm.goToPage(vm.pagination.pageNumber - 1); + vm.nextPage = () => vm.goToPage(vm.pagination.pageNumber + 1); + + vm.goToPage = (pageNumber) => { + vm.pagination.pageNumber = pageNumber; + vm.loading = true; + loadLogs().then(() => { + vm.loading = false; + }); + }; init(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html index 3a92c04a56..c6857f1eb1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html @@ -3,7 +3,6 @@ - Webhook key Date Url Event @@ -14,13 +13,13 @@ + ng-if="log.isSuccessStatusCode" + checked="true" + size="m" + title="{{ log.statusCode }}"> - + - {{ log.webhookKey }} {{ log.formattedLogDate }} {{ log.url }} {{ log.eventAlias }} @@ -28,4 +27,16 @@ + + + There are no items show in the list. + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.controller.js index 39d25c0205..b7940ac657 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.controller.js @@ -5,6 +5,7 @@ vm.close = close; vm.formatData = formatData; + vm.detectLanguage = detectLanguage; function formatData(data) { @@ -12,7 +13,7 @@ if (data.detectIsJson()) { try { - obj = Utilities.fromJson(data) + obj = JSON.stringify(Utilities.fromJson(data), null, 2); } catch (err) { obj = data; } @@ -21,6 +22,21 @@ return obj; } + function detectLanguage(headers, defaultLanguage) { + const matches = headers.match(/^Content-Type:\s*(?[a-z\/+.-]+)(\;?.*?)$/mi) + if (matches) { + const contentType = matches.groups["type"]; + if (contentType === "application/json") + return "JSON"; + if (contentType === "text/html") + return "HTML"; + if (contentType === "application/xml" || contentType === "text/xml") + return "XML"; + } + + return defaultLanguage || "TEXT"; + } + function close() { if ($scope.model && $scope.model.close) { $scope.model.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html index 1c07398dba..8654f8f47b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html @@ -21,9 +21,9 @@
    + ng-if="model.log.isSuccessStatusCode"> + ng-if="!model.log.isSuccessStatusCode">
    {{model.log.statusCode}}
    @@ -45,6 +45,10 @@
    {{model.log.retryCount}}
    + +
    {{model.log.webhookKey}}
    +
    + @@ -53,16 +57,14 @@
    {{model.log.requestHeaders}}
    - {{vm.formatData(model.log.requestBody) | json}} - + {{vm.formatData(model.log.requestBody)}}
    {{model.log.responseHeaders}}
    - {{vm.formatData(model.log.responseBody) | json}} - + {{vm.formatData(model.log.responseBody)}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js index a8dcf8658b..7dc99f84a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js @@ -10,7 +10,7 @@ vm.page.name = ""; vm.page.navigation = []; - let webhookUri = $routeParams.method; + let webhookUri = $routeParams.id; onInit(); @@ -33,8 +33,8 @@ { "name": vm.page.labels.webhooks, "icon": "icon-webhook", - "view": "views/webhooks/webhooks.html", - "active": webhookUri === 'overview', + "view": !webhookUri ? "views/webhooks/webhooks.html" : null, + "active": !webhookUri, "alias": "umbWebhooks", "action": function () { $location.path("/settings/webhooks/overview"); @@ -43,11 +43,11 @@ { "name": vm.page.labels.logs, "icon": "icon-box-alt", - "view": "views/webhooks/logs.html", + "view": webhookUri === 'logs' ? "views/webhooks/logs.html" : null, "active": webhookUri === 'logs', "alias": "umbWebhookLogs", "action": function () { - $location.path("/settings/webhooks/overview"); + $location.path("/settings/webhooks/overview/logs"); } } ]; From ba9ddd11da66b6ecdf72fdbfc660234d63843bb8 Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 10 May 2024 11:36:12 +0200 Subject: [PATCH 16/61] V13: Optimize custom MVC routing (#16218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/Umbraco.Core/Constants-Web.cs | 1 + .../UmbracoBuilderExtensions.cs | 1 + .../UmbracoApplicationBuilder.Website.cs | 3 +- .../Routing/EagerMatcherPolicy.cs | 229 ++++++++++++++++++ .../Routing/UmbracoRouteValueTransformer.cs | 28 --- 5 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 0c39c1b1b0..1364abac5e 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.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..3fe0814a15 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -0,0 +1,229 @@ +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 == "InstallController" + && 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] = "Install", + [Constants.Web.Routing.ActionToken] = "Index", + [Constants.Web.Routing.AreaToken] = Constants.Web.Mvc.InstallArea, + }); + + 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 2afba1e0bb..ee4195221c 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -139,23 +139,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) - { - return new RouteValueDictionary() - { - //TODO figure out constants - [ControllerToken] = "Install", - [ActionToken] = "Index", - [AreaToken] = Constants.Web.Mvc.InstallArea, - }; - } - - return null!; - } - // will be null for any client side requests like JS, etc... if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { @@ -172,17 +155,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) { From 18765465ae7ce5608d5e2e3b5c4a0d53030c6d1c Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 10 May 2024 11:36:12 +0200 Subject: [PATCH 17/61] V13: Optimize custom MVC routing (#16218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) --- src/Umbraco.Core/Constants-Web.cs | 1 + .../UmbracoBuilderExtensions.cs | 1 + .../UmbracoApplicationBuilder.Website.cs | 3 +- .../Routing/EagerMatcherPolicy.cs | 229 ++++++++++++++++++ .../Routing/UmbracoRouteValueTransformer.cs | 28 --- 5 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 0c39c1b1b0..1364abac5e 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.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..3fe0814a15 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -0,0 +1,229 @@ +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 == "InstallController" + && 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] = "Install", + [Constants.Web.Routing.ActionToken] = "Index", + [Constants.Web.Routing.AreaToken] = Constants.Web.Mvc.InstallArea, + }); + + 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 2afba1e0bb..ee4195221c 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -139,23 +139,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) - { - return new RouteValueDictionary() - { - //TODO figure out constants - [ControllerToken] = "Install", - [ActionToken] = "Index", - [AreaToken] = Constants.Web.Mvc.InstallArea, - }; - } - - return null!; - } - // will be null for any client side requests like JS, etc... if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { @@ -172,17 +155,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) { From ab32bac5d96695eebe4f333a9bd6fa8d8b820f71 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 13 May 2024 15:44:07 +0200 Subject: [PATCH 18/61] Property source level variation should only be applied when configured (#16270) --- .../Property.cs | 15 ++++++++----- .../PublishedContentVarianceTests.cs | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index 596bae2090..ed9f7277ef 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -25,7 +25,7 @@ internal class Property : PublishedPropertyBase // 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; @@ -88,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 @@ -104,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 @@ -148,9 +148,14 @@ internal class Property : PublishedPropertyBase 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; } 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) From 94cef504a3e10a6bc2b577e7d0ba3cd80af95175 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 13 May 2024 15:44:07 +0200 Subject: [PATCH 19/61] Property source level variation should only be applied when configured (#16270) (cherry picked from commit ab32bac5d96695eebe4f333a9bd6fa8d8b820f71) --- .../Property.cs | 15 ++++++++----- .../PublishedContentVarianceTests.cs | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index 596bae2090..ed9f7277ef 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -25,7 +25,7 @@ internal class Property : PublishedPropertyBase // 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; @@ -88,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 @@ -104,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 @@ -148,9 +148,14 @@ internal class Property : PublishedPropertyBase 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; } 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) From c17d4e1a600098ec524e4126f4395255476bc33f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 May 2024 08:37:51 +0200 Subject: [PATCH 20/61] Merge pull request from GHSA-j74q-mv2c-rxmp --- src/Umbraco.Core/Routing/WebPath.cs | 24 ++++++ .../Controllers/ImagesController.cs | 3 +- .../Controllers/PreviewController.cs | 4 +- .../Umbraco.Core/Routing/WebPathTests.cs | 83 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) 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.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index e718696ae3..71fa9625a6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; @@ -123,7 +124,7 @@ public class ImagesController : UmbracoAuthorizedApiController private bool IsAllowed(string encodedImagePath) { - if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative)) + if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative)) { return true; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 19ca323d9d..17875c2950 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -152,8 +153,7 @@ public class PreviewController : Controller // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName); - if (Uri.IsWellFormedUriString(redir, UriKind.Relative) - && redir.StartsWith("//") == false + if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative) && Uri.TryCreate(redir, UriKind.Relative, out Uri? url)) { return Redirect(url.ToString()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs index 098e047981..acfa4ffe6f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs @@ -31,4 +31,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); + } From d8df405db4ea884bb4b96f088d10d9a2070cf024 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 May 2024 08:37:51 +0200 Subject: [PATCH 21/61] Merge pull request from GHSA-j74q-mv2c-rxmp --- src/Umbraco.Core/Routing/WebPath.cs | 24 ++++++ .../Controllers/ImagesController.cs | 3 +- .../Controllers/PreviewController.cs | 4 +- .../Umbraco.Core/Routing/WebPathTests.cs | 83 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) 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.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index 90ef6e6cf4..b70661c0af 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; @@ -122,7 +123,7 @@ public class ImagesController : UmbracoAuthorizedApiController private bool IsAllowed(string encodedImagePath) { - if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative)) + if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative)) { return true; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 19ca323d9d..17875c2950 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -152,8 +153,7 @@ public class PreviewController : Controller // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName); - if (Uri.IsWellFormedUriString(redir, UriKind.Relative) - && redir.StartsWith("//") == false + if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative) && Uri.TryCreate(redir, UriKind.Relative, out Uri? url)) { return Redirect(url.ToString()); 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); + } From 5f24de308584b9771240a6db1a34630a5114c450 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 May 2024 08:37:51 +0200 Subject: [PATCH 22/61] Merge pull request from GHSA-j74q-mv2c-rxmp --- src/Umbraco.Core/Routing/WebPath.cs | 24 ++++++ .../Controllers/ImagesController.cs | 3 +- .../Controllers/PreviewController.cs | 4 +- .../Umbraco.Core/Routing/WebPathTests.cs | 83 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) 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.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index 90ef6e6cf4..b70661c0af 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; @@ -122,7 +123,7 @@ public class ImagesController : UmbracoAuthorizedApiController private bool IsAllowed(string encodedImagePath) { - if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative)) + if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative)) { return true; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 19ca323d9d..17875c2950 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -152,8 +153,7 @@ public class PreviewController : Controller // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName); - if (Uri.IsWellFormedUriString(redir, UriKind.Relative) - && redir.StartsWith("//") == false + if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative) && Uri.TryCreate(redir, UriKind.Relative, out Uri? url)) { return Redirect(url.ToString()); 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); + } From 0f3160f727f33c10141f3b9cb55e9d7b97e805a1 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 22 May 2024 12:17:04 +0200 Subject: [PATCH 23/61] Move publishing notification after validation (#16331) --- src/Umbraco.Core/Services/ContentService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 839a5afe90..e723bfcdd2 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1487,6 +1487,14 @@ public class ContentService : RepositoryService, IContentService notificationState); if (publishResult.Success) { + // raise Publishing notification + if (scope.Notifications.PublishCancelable( + new ContentPublishingNotification(content, eventMessages).WithState(notificationState))) + { + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, eventMessages, content); + } + // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages); @@ -3065,14 +3073,6 @@ public class ContentService : RepositoryService, IContentService IReadOnlyCollection allLangs, IDictionary? notificationState) { - // raise Publishing notification - if (scope.Notifications.PublishCancelable( - new ContentPublishingNotification(content, evtMsgs).WithState(notificationState))) - { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - var variesByCulture = content.ContentType.VariesByCulture(); // If it's null it's invariant From fd2138c1fd0dfd74e9bd5152a35fff3a00c95ff6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 22 May 2024 12:21:56 +0200 Subject: [PATCH 24/61] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index fcf4a18f98..c16578e335 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.3.1", + "version": "13.3.2", "assemblyVersion": { "precision": "build" }, From eb6bb99eafaf21f4bf9d241b7385aa1572b6f32d Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 22 May 2024 13:29:17 +0200 Subject: [PATCH 25/61] Ensure there is always at least 1 valid candidate (#16344) --- .../Routing/EagerMatcherPolicy.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index 3fe0814a15..4c68d3428e 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -72,7 +72,8 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } // If there's only one candidate, we don't need to do anything. - if (candidates.Count < 2) + var candidateCount = candidates.Count; + if (candidateCount < 2) { return; } @@ -85,6 +86,14 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy RouteEndpoint? dynamicEndpoint = null; for (var i = 0; i < candidates.Count; i++) { + if (candidates.IsValidCandidate(i) is false) + { + // If the candidate is not valid we reduce the candidate count so we can later ensure that there is always + // at least 1 candidate. + candidateCount -= 1; + continue; + } + CandidateState candidate = candidates[i]; // If it's not a RouteEndpoint there's not much we can do to count it in the order. @@ -123,7 +132,7 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy // 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) + if (dynamicEndpoint is not null && dynamicId is not null && dynamicEndpoint.Order > lowestOrder && candidateCount > 1) { candidates.SetValidity(dynamicId.Value, false); } From 5f082df3ab406fe6c0d424d8eec9a6fc35fdaa43 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 22 May 2024 13:29:17 +0200 Subject: [PATCH 26/61] Ensure there is always at least 1 valid candidate (#16344) (cherry picked from commit eb6bb99eafaf21f4bf9d241b7385aa1572b6f32d) --- .../Routing/EagerMatcherPolicy.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index 3fe0814a15..4c68d3428e 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -72,7 +72,8 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } // If there's only one candidate, we don't need to do anything. - if (candidates.Count < 2) + var candidateCount = candidates.Count; + if (candidateCount < 2) { return; } @@ -85,6 +86,14 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy RouteEndpoint? dynamicEndpoint = null; for (var i = 0; i < candidates.Count; i++) { + if (candidates.IsValidCandidate(i) is false) + { + // If the candidate is not valid we reduce the candidate count so we can later ensure that there is always + // at least 1 candidate. + candidateCount -= 1; + continue; + } + CandidateState candidate = candidates[i]; // If it's not a RouteEndpoint there's not much we can do to count it in the order. @@ -123,7 +132,7 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy // 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) + if (dynamicEndpoint is not null && dynamicId is not null && dynamicEndpoint.Order > lowestOrder && candidateCount > 1) { candidates.SetValidity(dynamicId.Value, false); } From 696a71166cd4d4e01ab7fa7083ae8a59bbb6b6eb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 23 May 2024 10:14:37 +0200 Subject: [PATCH 27/61] Ensure ufprt-token requests are handle in the UmbracoRouteValueTransformer (#16347) --- src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index 4c68d3428e..05290227b3 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Extensions; +using HttpRequestExtensions = Umbraco.Extensions.HttpRequestExtensions; namespace Umbraco.Cms.Web.Website.Routing; @@ -71,9 +72,10 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } } - // If there's only one candidate, we don't need to do anything. + // If there's only one candidate, or the request has the ufprt-token, we don't need to do anything . + // The ufprt-token is handled by the the and should not be discarded. var candidateCount = candidates.Count; - if (candidateCount < 2) + if (candidateCount < 2 || string.IsNullOrEmpty(httpContext.Request.GetUfprt()) is false) { return; } From 5795cf119a0b5ab148019ed39d92ecbc5bd7b14c Mon Sep 17 00:00:00 2001 From: Johan Runsten Date: Fri, 12 Apr 2024 23:37:19 +0200 Subject: [PATCH 28/61] Typo when getting query parm --- .../src/components/external-login-provider.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts b/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts index b48943e019..0729f97027 100644 --- a/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts @@ -73,7 +73,7 @@ export class UmbExternalLoginProviderElement extends LitElement { @property({attribute: 'external-login-url'}) set externalLoginUrl(value: string) { const tempUrl = new URL(value, window.location.origin); - const searchParams = new URLSearchParams(tempUrl.search); + const searchParams = new URLSearchParams(window.location.search); tempUrl.searchParams.append('redirectUrl', decodeURIComponent(searchParams.get('returnPath') ?? '')); this.#externalLoginUrl = tempUrl.pathname + tempUrl.search; } From 0aaac78cfa119fd4ff23eba9b3e92d0bd8510358 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 3 Jun 2024 11:23:25 +0200 Subject: [PATCH 29/61] A bunch of minor performance optimizations (#16335) * Do not execute query if no macros found * Request cache the permission lookup * Unbreak change by adding obsolete ctor * Clean up * Wrap indexing for delivery API in a scope * Do not ask options every time for the timeout, instead listen for updates * Lookup content types once instead of one by one * Use TryGetValue instead * Do a distinct on user ids before building index, to avoid issue with more than 2100 parameters * Don't map ContentDto (it's unused) * Introduce request bound block editor element cache --------- Co-authored-by: kjac --- ...ServerEFCoreDistributedLockingMechanism.cs | 22 +++++----- ...SqliteEFCoreDistributedLockingMechanism.cs | 18 ++++---- .../SqlServerDistributedLockingMechanism.cs | 19 ++++---- .../SqliteDistributedLockingMechanism.cs | 18 ++++---- .../ContentEditing/ContentItemDisplay.cs | 1 + src/Umbraco.Core/Services/UserService.cs | 43 +++++++++++++++---- .../BlockEditorElementTypeCache.cs | 32 ++++++++++++++ .../IBlockEditorElementTypeCache.cs | 8 ++++ .../UmbracoBuilder.CoreServices.cs | 3 ++ .../Examine/ContentValueSetBuilder.cs | 4 +- .../DeliveryApiContentIndexValueSetBuilder.cs | 36 +++++++++++++++- .../PropertyEditors/BlockEditorValidator.cs | 5 ++- .../BlockEditorValidatorBase.cs | 11 ++--- .../PropertyEditors/BlockEditorValues.cs | 22 +++++----- .../BlockGridPropertyEditorBase.cs | 7 +-- .../BlockListPropertyEditorBase.cs | 7 +-- .../RichTextEditorBlockValidator.cs | 5 ++- .../PropertyEditors/RichTextPropertyEditor.cs | 11 ++--- .../Templates/HtmlMacroParameterParser.cs | 19 ++++---- .../Mapping/ContentMapDefinition.cs | 13 ++---- .../DataValueEditorReuseTests.cs | 3 +- 21 files changed, 210 insertions(+), 97 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs create mode 100644 src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs index 4652c513a3..38cdeef114 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking; internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLockingMechanism where T : DbContext { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger> _logger; private readonly Lazy> _scopeAccessor; // Hooray it's a circular dependency. @@ -32,27 +32,29 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki { _logger = logger; _scopeAccessor = scopeAccessor; - _globalSettings = globalSettings; - _connectionStrings = connectionStrings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } public bool HasActiveRelatedScope => _scopeAccessor.Value.AmbientScope is not null; /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null; + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null; /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } /// public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } @@ -168,9 +170,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki "A transaction with minimum ReadCommitted isolation level is required."); } - await dbContext.Database.ExecuteSqlRawAsync($"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};"); - - var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); + var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); if (rowsAffected == 0) { diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs index 8d92ec0e03..23b3d8d410 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqliteEFCoreDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking; internal class SqliteEFCoreDistributedLockingMechanism : IDistributedLockingMechanism where T : DbContext { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger> _logger; private readonly Lazy> _efCoreScopeAccessor; @@ -29,27 +29,29 @@ internal class SqliteEFCoreDistributedLockingMechanism : IDistributedLockingM { _logger = logger; _efCoreScopeAccessor = efCoreScopeAccessor; - _connectionStrings = connectionStrings; - _globalSettings = globalSettings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } public bool HasActiveRelatedScope => _efCoreScopeAccessor.Value.AmbientScope is not null; /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null; + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null; // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } // With journal_mode=wal only a single write transaction can exist at a time. public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 77975e8f31..a7f183e57a 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -17,8 +17,8 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services; /// public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. @@ -33,25 +33,28 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { _logger = logger; _scopeAccessor = scopeAccessor; - _globalSettings = globalSettings; - _connectionStrings = connectionStrings; + _globalSettings = globalSettings.CurrentValue; + _connectionStrings = connectionStrings.CurrentValue; + globalSettings.OnChange(x => _globalSettings = x); + connectionStrings.OnChange(x => _connectionStrings = x); + } /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } /// public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index 54e30d6fa6..f43a1eff05 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly IOptionsMonitor _connectionStrings; - private readonly IOptionsMonitor _globalSettings; + private ConnectionStrings _connectionStrings; + private GlobalSettings _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; @@ -29,25 +29,27 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { _logger = logger; _scopeAccessor = scopeAccessor; - _connectionStrings = connectionStrings; - _globalSettings = globalSettings; + _connectionStrings = connectionStrings.CurrentValue; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(x=>_globalSettings = x); + connectionStrings.OnChange(x=>_connectionStrings = x); } /// - public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - string.Equals(_connectionStrings.CurrentValue.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); + public bool Enabled => _connectionStrings.IsConnectionStringConfigured() && + string.Equals(_connectionStrings.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); } // With journal_mode=wal only a single write transaction can exist at a time. public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) { - obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout; return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index d1a8d10970..4e207702a0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -194,6 +194,7 @@ public class ContentItemDisplay : /// This is not used for outgoing model information. /// [IgnoreDataMember] + [Obsolete("No longer used. Will be removed in V15.")] public ContentPropertyCollectionDto? ContentDto { get; set; } /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 7f839e81d1..e0f65cdd5c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,9 +1,12 @@ using System.Data.Common; using System.Globalization; using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; @@ -25,8 +28,11 @@ internal class UserService : RepositoryService, IUserService private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; private readonly IUserGroupRepository _userGroupRepository; + private readonly IRequestCache _requestCache; private readonly IUserRepository _userRepository; + + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] public UserService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -35,11 +41,26 @@ internal class UserService : RepositoryService, IUserService IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) + : this(provider, loggerFactory, eventMessagesFactory, runtimeState, userRepository, userGroupRepository, globalSettings, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public UserService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRuntimeState runtimeState, + IUserRepository userRepository, + IUserGroupRepository userGroupRepository, + IOptions globalSettings, + IRequestCache requestCache) : base(provider, loggerFactory, eventMessagesFactory) { _runtimeState = runtimeState; _userRepository = userRepository; _userGroupRepository = userGroupRepository; + _requestCache = requestCache; _globalSettings = globalSettings.Value; _logger = loggerFactory.CreateLogger(); } @@ -1125,17 +1146,23 @@ internal class UserService : RepositoryService, IUserService /// Path to check permissions for public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) { - var nodeIds = path?.GetIdsFromPathReversed(); - - if (nodeIds is null || nodeIds.Length == 0 || user is null) + var result = (EntityPermissionSet?)_requestCache.Get($"{nameof(GetPermissionsForPath)}|{path}|{user?.Id}", () => { - return EntityPermissionSet.Empty(); - } + var nodeIds = path?.GetIdsFromPathReversed(); - // collect all permissions structures for all nodes for all groups belonging to the user - EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + if (nodeIds is null || nodeIds.Length == 0 || user is null) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups belonging to the user + EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + }); + + return result ?? EntityPermissionSet.Empty(); - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } /// diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5cbf0e6dc2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache +{ + private readonly IContentTypeService _contentTypeService; + private readonly AppCaches _appCaches; + + public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches) + { + _contentTypeService = contentTypeService; + _appCaches = appCaches; + } + + public IEnumerable GetAll(IEnumerable keys) + { + // TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded + + const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes"; + IEnumerable? cachedElements = _appCaches.RequestCache.GetCacheItem>(cacheKey); + if (cachedElements is null) + { + cachedElements = _contentTypeService.GetAllElementTypes(); + _appCaches.RequestCache.Set(cacheKey, cachedElements); + } + + return cachedElements.Where(elementType => keys.Contains(elementType.Key)); + } +} diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5ab1dc49af --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +public interface IBlockEditorElementTypeCache +{ + IEnumerable GetAll(IEnumerable keys); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 10fbcc1207..77194cef2e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; @@ -235,6 +236,8 @@ public static partial class UmbracoBuilderExtensions builder.AddDeliveryApiCoreServices(); builder.Services.AddTransient(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index 860c6199f7..98c28f92df 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -128,9 +128,9 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal // processing below instead of one by one. using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { - creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).Distinct().ToArray()) .ToDictionary(x => x.Id, x => x); - writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).Distinct().ToArray()) .ToDictionary(x => x.Id, x => x); scope.Complete(); } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs index e8226d994c..1bbc1d02cf 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -21,6 +22,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte private readonly IDeliveryApiContentIndexFieldDefinitionBuilder _deliveryApiContentIndexFieldDefinitionBuilder; private readonly IMemberService _memberService; private readonly IDeliveryApiCompositeIdHandler _deliveryApiCompositeIdHandler; + private readonly ICoreScopeProvider _coreScopeProvider; private DeliveryApiSettings _deliveryApiSettings; [Obsolete("Please use ctor that takes an IDeliveryApiCompositeIdHandler. Scheduled for removal in v15")] @@ -40,8 +42,33 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte deliveryApiContentIndexFieldDefinitionBuilder, deliveryApiSettings, memberService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) { + } + [Obsolete("Please use ctor that takes an IDeliveryApiCompositeIdHandler. Scheduled for removal in v15")] + public DeliveryApiContentIndexValueSetBuilder( + ContentIndexHandlerCollection contentIndexHandlerCollection, + IContentService contentService, + IPublicAccessService publicAccessService, + ILogger logger, + IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder, + IOptionsMonitor deliveryApiSettings, + IMemberService memberService, + IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandle) + :this( + contentIndexHandlerCollection, + contentService, + publicAccessService, + logger, + deliveryApiContentIndexFieldDefinitionBuilder, + deliveryApiSettings, + memberService, + deliveryApiCompositeIdHandle, + StaticServiceProvider.Instance.GetRequiredService()) + { + } public DeliveryApiContentIndexValueSetBuilder( @@ -52,7 +79,8 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder, IOptionsMonitor deliveryApiSettings, IMemberService memberService, - IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandler) + IDeliveryApiCompositeIdHandler deliveryApiCompositeIdHandler, + ICoreScopeProvider coreScopeProvider) { _contentIndexHandlerCollection = contentIndexHandlerCollection; _publicAccessService = publicAccessService; @@ -60,6 +88,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte _deliveryApiContentIndexFieldDefinitionBuilder = deliveryApiContentIndexFieldDefinitionBuilder; _memberService = memberService; _deliveryApiCompositeIdHandler = deliveryApiCompositeIdHandler; + _coreScopeProvider = coreScopeProvider; _contentService = contentService; _deliveryApiSettings = deliveryApiSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); @@ -68,6 +97,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte /// public IEnumerable GetValueSets(params IContent[] contents) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); FieldDefinitionCollection fieldDefinitions = _deliveryApiContentIndexFieldDefinitionBuilder.Build(); foreach (IContent content in contents.Where(CanIndex)) { @@ -101,6 +131,8 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte yield return new ValueSet(_deliveryApiCompositeIdHandler.IndexId(content.Id, indexCulture), IndexTypes.Content, content.ContentType.Alias, indexValues); } + + scope.Complete(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index 8e17c6c477..fb58ab042d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; @@ -13,8 +14,8 @@ internal class BlockEditorValidator : BlockEditorValidatorBase public BlockEditorValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, - IContentTypeService contentTypeService) - : base(propertyValidationService, contentTypeService) + IBlockEditorElementTypeCache elementTypeCache) + : base(propertyValidationService, elementTypeCache) => _blockEditorValues = blockEditorValues; protected override IEnumerable GetElementTypeValidation(object? value) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 977d235229..4d131bc818 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; @@ -6,11 +7,11 @@ namespace Umbraco.Cms.Core.PropertyEditors; internal abstract class BlockEditorValidatorBase : ComplexEditorValidator { - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; - protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IContentTypeService contentTypeService) + protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IBlockEditorElementTypeCache elementTypeCache) : base(propertyValidationService) - => _contentTypeService = contentTypeService; + => _elementTypeCache = elementTypeCache; protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData) { @@ -18,7 +19,7 @@ internal abstract class BlockEditorValidatorBase : ComplexEditorValidator // need to validate that data for each property especially for things like 'required' data to work. // Lookup all element types for all content/settings and then we can populate any empty properties. var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + var allElementTypes = _elementTypeCache.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); foreach (BlockItemData row in allElements) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index 3270351838..98dbb85889 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -2,9 +2,9 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -15,13 +15,13 @@ namespace Umbraco.Cms.Core.PropertyEditors; internal class BlockEditorValues { private readonly BlockEditorDataConverter _dataConverter; - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) + public BlockEditorValues(BlockEditorDataConverter dataConverter, IBlockEditorElementTypeCache elementTypeCache, ILogger logger) { _dataConverter = dataConverter; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _logger = logger; } @@ -55,10 +55,14 @@ internal class BlockEditorValues var contentTypePropertyTypes = new Dictionary>(); // filter out any content that isn't referenced in the layout references + IEnumerable contentTypeKeys = blockEditorData.BlockValue.ContentData.Select(x => x.ContentTypeKey) + .Union(blockEditorData.BlockValue.SettingsData.Select(x => x.ContentTypeKey)).Distinct(); + IDictionary contentTypesDictionary = _elementTypeCache.GetAll(contentTypeKeys).ToDictionary(x=>x.Key); + foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) { - ResolveBlockItemData(block, contentTypePropertyTypes); + ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } // filter out any settings that isn't referenced in the layout references @@ -66,7 +70,7 @@ internal class BlockEditorValues blockEditorData.References.Any(r => r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) { - ResolveBlockItemData(block, contentTypePropertyTypes); + ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } // remove blocks that couldn't be resolved @@ -76,12 +80,10 @@ internal class BlockEditorValues return blockEditorData; } - private IContentType? GetElementType(BlockItemData item) => _contentTypeService.Get(item.ContentTypeKey); - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) + private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes, IDictionary contentTypesDictionary) { - IContentType? contentType = GetElementType(block); - if (contentType == null) + if (contentTypesDictionary.TryGetValue(block.ContentTypeKey, out IContentType? contentType) is false) { return false; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index fe72d83927..8d6e080b74 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -58,12 +59,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 5d2c968c72..690251467c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -55,7 +56,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeConfigurationCache dataTypeConfigurationCache, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, ILocalizedTextService textService, ILogger logger, IShortStringHelper shortStringHelper, @@ -64,8 +65,8 @@ public abstract class BlockListPropertyEditorBase : DataEditor IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs index 01d10e46f0..dc4775f6b7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -14,10 +15,10 @@ internal class RichTextEditorBlockValidator : BlockEditorValidatorBase public RichTextEditorBlockValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IJsonSerializer jsonSerializer, ILogger logger) - : base(propertyValidationService, contentTypeService) + : base(propertyValidationService, elementTypeCache) { _blockEditorValues = blockEditorValues; _jsonSerializer = jsonSerializer; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 2447803277..ee37d8c63b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; @@ -171,7 +172,7 @@ public class RichTextPropertyEditor : DataEditor private readonly IHtmlMacroParameterParser _macroParameterParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IJsonSerializer _jsonSerializer; - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; public RichTextPropertyValueEditor( @@ -189,7 +190,7 @@ public class RichTextPropertyEditor : DataEditor IIOHelper ioHelper, IHtmlSanitizer htmlSanitizer, IHtmlMacroParameterParser macroParameterParser, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) @@ -200,11 +201,11 @@ public class RichTextPropertyEditor : DataEditor _pastedImages = pastedImages; _htmlSanitizer = htmlSanitizer; _macroParameterParser = macroParameterParser; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _jsonSerializer = jsonSerializer; _logger = logger; - Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), contentTypeService, jsonSerializer, logger)); + Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger)); } /// @@ -392,6 +393,6 @@ public class RichTextPropertyEditor : DataEditor } private BlockEditorValues CreateBlockEditorValues() - => new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger); + => new(new RichTextEditorBlockDataConverter(), _elementTypeCache, _logger); } } diff --git a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs index 6c7445d2da..bb8c4d0d86 100644 --- a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs +++ b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs @@ -55,10 +55,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser macroAlias, new Dictionary(macroAttributes, StringComparer.OrdinalIgnoreCase)))); - foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) - { - yield return umbracoEntityReference; - } + return foundMacros.Count > 0 + ? GetUmbracoEntityReferencesFromMacros(foundMacros) + : Enumerable.Empty(); } /// @@ -82,10 +81,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser } } - foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) - { - yield return umbracoEntityReference; - } + return foundMacros.Count > 0 + ? GetUmbracoEntityReferencesFromMacros(foundMacros) + : Enumerable.Empty(); } private IEnumerable GetUmbracoEntityReferencesFromMacros( @@ -96,6 +94,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser yield break; } + IEnumerable uniqueMacroAliases = macros.Select(f => f.Item1).Distinct(); // TODO: Tracking Macro references @@ -103,7 +102,9 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser var foundMacroUmbracoEntityReferences = new List(); // Get all the macro configs in one hit for these unique macro aliases - this is now cached with a custom cache policy - IEnumerable macroConfigs = macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray()); + IEnumerable macroConfigs = uniqueMacroAliases.Any() + ? macroWithAliasService.GetAll(uniqueMacroAliases.WhereNotNull().ToArray()) + : Enumerable.Empty(); foreach (Tuple> macro in macros) { diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 0bc4e9c1b7..ae48169943 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -128,7 +128,7 @@ internal class ContentMapDefinition : IMapDefinition target.AdditionalPreviewUrls = source.AdditionalPreviewUrls; } - // Umbraco.Code.MapAll + // Umbraco.Code.MapAll -ContentDto private void Map(ContentItemDisplay source, ContentItemDisplayWithSchedule target, MapperContext context) { foreach (KeyValuePair additionalData in source.AdditionalData) @@ -140,7 +140,6 @@ internal class ContentMapDefinition : IMapDefinition target.AllowedTemplates = source.AllowedTemplates; target.AllowPreview = source.AllowPreview; target.ContentApps = source.ContentApps; - target.ContentDto = source.ContentDto; target.ContentTypeAlias = source.ContentTypeAlias; target.ContentTypeId = source.ContentTypeId; target.ContentTypeKey = source.ContentTypeKey; @@ -207,7 +206,7 @@ internal class ContentMapDefinition : IMapDefinition } } - // Umbraco.Code.MapAll + // Umbraco.Code.MapAll -ContentDto private static void Map(ContentItemDisplayWithSchedule source, ContentItemDisplay target, MapperContext context) { foreach (KeyValuePair additionalData in source.AdditionalData) @@ -219,7 +218,6 @@ internal class ContentMapDefinition : IMapDefinition target.AllowedTemplates = source.AllowedTemplates; target.AllowPreview = source.AllowPreview; target.ContentApps = source.ContentApps; - target.ContentDto = source.ContentDto; target.ContentTypeAlias = source.ContentTypeAlias; target.ContentTypeId = source.ContentTypeId; target.ContentTypeKey = source.ContentTypeKey; @@ -253,7 +251,7 @@ internal class ContentMapDefinition : IMapDefinition private static void Map(IContent source, ContentPropertyCollectionDto target, MapperContext context) => target.Properties = context.MapEnumerable(source.Properties).WhereNotNull(); - // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent + // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent -ContentDto private void Map(IContent source, ContentItemDisplay target, MapperContext context) where TVariant : ContentVariantDisplay { @@ -300,11 +298,6 @@ internal class ContentMapDefinition : IMapDefinition target.Updater = _commonMapper.GetCreator(source, context); target.Urls = GetUrls(source); target.Variants = _contentVariantMapper.Map(source, context); - - target.ContentDto = new ContentPropertyCollectionDto - { - Properties = context.MapEnumerable(source.Properties).WhereNotNull() - }; } // Umbraco.Code.MapAll -Segment -Language -DisplayName -AdditionalPreviewUrls diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 0874b85987..b04acdbbc4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; @@ -44,7 +45,7 @@ public class DataValueEditorReuseTests _propertyEditorCollection, _dataValueReferenceFactories, Mock.Of(), - Mock.Of(), + Mock.Of(), Mock.Of(), Mock.Of>(), Mock.Of(), From 100f2c3bcd3461b7b6c41e8c654ae130d22cc6fa Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 3 Jun 2024 12:03:40 +0200 Subject: [PATCH 30/61] Add check to ensure that RenderControllers and SurfaceControllers are always routed through the UmbracoRouteValueTransforms (#16540) --- .../Routing/EagerMatcherPolicy.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index 05290227b3..bb5cb52a4d 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using System.Reflection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; @@ -8,8 +9,8 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; -using HttpRequestExtensions = Umbraco.Extensions.HttpRequestExtensions; namespace Umbraco.Cms.Web.Website.Routing; @@ -104,6 +105,18 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy continue; } + // We have to ensure that none of the candidates is a render controller or surface controller + // Normally these shouldn't be statically routed, however some people do it. + // So we should probably be friendly and check for it. + // Do not add this to V14. + ControllerActionDescriptor? controllerDescriptor = routeEndpoint.Metadata.GetMetadata(); + TypeInfo? controllerTypeInfo = controllerDescriptor?.ControllerTypeInfo; + if (controllerTypeInfo is not null && + (controllerTypeInfo.IsType() || controllerTypeInfo.IsType())) + { + return; + } + if (routeEndpoint.Order < lowestOrder) { // We have to ensure that the route is valid for the current request method. From c3e7dad236aeab7f824abf2b7ddcca4311794869 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:09:37 +0200 Subject: [PATCH 31/61] bumb to rc version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b9fa941e03..529d4b8c65 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0", + "version": "13.4.0-rc", "assemblyVersion": { "precision": "build" }, From 32912b0c35c045c2f0c1cdad443cef2717d04ac5 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:34:43 +0200 Subject: [PATCH 32/61] Update to query to SqlRaw (#16542) --- .../Locking/SqlServerEFCoreDistributedLockingMechanism.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs index 38cdeef114..f037335837 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs @@ -170,7 +170,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki "A transaction with minimum ReadCommitted isolation level is required."); } - var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); + var rowsAffected = await dbContext.Database.ExecuteSqlRawAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}"); if (rowsAffected == 0) { From 3dace4fc9d8fd68b829c49a7746a302b37bd3bea Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 4 Jun 2024 10:32:37 +0200 Subject: [PATCH 33/61] RTE and media picker should route medias the same way in the Delivery API (#16550) * RTE and media picker should route medias the same way in the Delivery API * Fix failing unit test * Fixed failing tests --- .../DeliveryApi/ApiMediaUrlProvider.cs | 2 +- .../DeliveryApi/ApiRichTextElementParser.cs | 19 ++----------------- .../DeliveryApi/ApiRichTextMarkupParser.cs | 5 ++--- .../DeliveryApi/ApiRichTextParserBase.cs | 11 +++++------ .../DeliveryApi/ApiMediaUrlProviderTests.cs | 2 +- .../DeliveryApi/RichTextParserTests.cs | 11 +++++------ 6 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs b/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs index 20373b1d3b..f8ebee826b 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs @@ -17,6 +17,6 @@ public sealed class ApiMediaUrlProvider : IApiMediaUrlProvider throw new ArgumentException("Media URLs can only be generated from Media items.", nameof(media)); } - return _publishedUrlProvider.GetMediaUrl(media, UrlMode.Relative); + return _publishedUrlProvider.GetMediaUrl(media); } } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index eeb279e1b7..eed3b848eb 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -22,28 +22,13 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich private const string TextNodeName = "#text"; private const string CommentNodeName = "#comment"; - [Obsolete($"Please use the constructor that accepts {nameof(IApiElementBuilder)}. Will be removed in V15.")] public ApiRichTextElementParser( IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - ILogger logger) - : this( - apiContentRouteBuilder, - publishedUrlProvider, - publishedSnapshotAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - logger) - { - } - - public ApiRichTextElementParser( - IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, + IApiMediaUrlProvider mediaUrlProvider, IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiElementBuilder apiElementBuilder, ILogger logger) - : base(apiContentRouteBuilder, publishedUrlProvider) + : base(apiContentRouteBuilder, mediaUrlProvider) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _apiElementBuilder = apiElementBuilder; diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index f7eeba0f18..42c8829868 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DeliveryApi; @@ -15,10 +14,10 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT public ApiRichTextMarkupParser( IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, + IApiMediaUrlProvider mediaUrlProvider, IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger) - : base(apiContentRouteBuilder, publishedUrlProvider) + : base(apiContentRouteBuilder, mediaUrlProvider) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _logger = logger; diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index 0509105b05..7723fc835c 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -4,19 +4,18 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; namespace Umbraco.Cms.Infrastructure.DeliveryApi; internal abstract partial class ApiRichTextParserBase { private readonly IApiContentRouteBuilder _apiContentRouteBuilder; - private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IApiMediaUrlProvider _apiMediaUrlProvider; - protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IPublishedUrlProvider publishedUrlProvider) + protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider apiMediaUrlProvider) { _apiContentRouteBuilder = apiContentRouteBuilder; - _publishedUrlProvider = publishedUrlProvider; + _apiMediaUrlProvider = apiMediaUrlProvider; } protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, Action handleContentRoute, Action handleMediaUrl, Action handleInvalidLink) @@ -52,7 +51,7 @@ internal abstract partial class ApiRichTextParserBase if (media != null) { handled = true; - handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); } break; @@ -77,7 +76,7 @@ internal abstract partial class ApiRichTextParserBase return; } - handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); } [GeneratedRegex("{localLink:(?umb:.+)}")] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs index e1900d203f..0dfffbf638 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs @@ -21,7 +21,7 @@ public class ApiMediaUrlProviderTests : PropertyValueConverterTests var publishedUrlProvider = new Mock(); publishedUrlProvider - .Setup(p => p.GetMediaUrl(content.Object, UrlMode.Relative, It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(p => p.GetMediaUrl(content.Object, UrlMode.Default, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(publishedUrl); var apiMediaUrlProvider = new ApiMediaUrlProvider(publishedUrlProvider.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index b7712b5346..a2522a5ecd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -9,7 +9,6 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Infrastructure.DeliveryApi; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -474,7 +473,7 @@ public class RichTextParserTests : PropertyValueConverterTests Mock.Of>()); } - private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IPublishedUrlProvider urlProvider) + private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IApiMediaUrlProvider apiMediaUrlProvider) { var contentMock = new Mock(); contentMock.SetupGet(m => m.Key).Returns(_contentKey); @@ -502,14 +501,14 @@ public class RichTextParserTests : PropertyValueConverterTests .Setup(m => m.Build(contentMock.Object, null)) .Returns(new ApiContentRoute("/some-content-path", new ApiContentStartItem(_contentRootKey, "the-root-path"))); - var urlProviderMock = new Mock(); - urlProviderMock - .Setup(m => m.GetMediaUrl(mediaMock.Object, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + var apiMediaUrlProviderMock = new Mock(); + apiMediaUrlProviderMock + .Setup(m => m.GetUrl(mediaMock.Object)) .Returns("/some-media-url"); routeBuilder = routeBuilderMock.Object; snapshotAccessor = snapshotAccessorMock.Object; - urlProvider = urlProviderMock.Object; + apiMediaUrlProvider = apiMediaUrlProviderMock.Object; } private IPublishedElement CreateElement(Guid id, int propertyValue) From ae7db56f77bc1b3f831cd009f9d66624df14f00b Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:25:57 +0200 Subject: [PATCH 34/61] Decreased retry count (#16554) --- tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts index f3d94a05f7..e72026e0e8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts @@ -21,7 +21,7 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 5 : 2, + retries: process.env.CI ? 2 : 1, // We don't want to run parallel, as tests might differ in state workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ From 78bf04ef67882cc8ee095ea7685da929d2b30d8e Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:56:22 +0200 Subject: [PATCH 35/61] bump version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 529d4b8c65..b9fa941e03 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0-rc", + "version": "13.4.0", "assemblyVersion": { "precision": "build" }, From 5f8eac0464779c7601a22b9737c2217a66f737f7 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 6 Jun 2024 10:22:18 +0200 Subject: [PATCH 36/61] bump version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b9fa941e03..288af47e08 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0", + "version": "13.5.0-rc", "assemblyVersion": { "precision": "build" }, From 5ae5fe338a96098c8bb3f97cdffb423b5c352e56 Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 7 Jun 2024 12:49:13 +0200 Subject: [PATCH 37/61] V13: Set request culture for VirtualPageController (#16572) * Rename FindDomain to FindAndSetDomain * Ensure VariationContext and PublishedRequest is updated for virtual page controller --- src/Umbraco.Core/Routing/IPublishedRouter.cs | 33 ++++++++++++++ src/Umbraco.Core/Routing/PublishedRouter.cs | 43 ++++++++++++++----- .../UmbracoVirtualPageFilterAttribute.cs | 8 ++++ .../Routing/UmbracoVirtualPageRoute.cs | 1 + .../ContentFinderByAliasWithDomainsTests.cs | 2 +- .../ContentFinderByUrlWithDomainsTests.cs | 4 +- .../Routing/DomainsAndCulturesTests.cs | 6 +-- .../Routing/UrlsWithNestedDomains.cs | 2 +- 8 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index 53a07ff325..a01e17c94d 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -47,4 +47,37 @@ public interface IPublishedRouter /// /// Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest and VariationContext accordingly. + /// + /// + /// This method is used for VirtualPage routing. + /// + /// + /// In this case we do not want to run the entire routing pipeline since ContentFinders are not needed here. + /// However, we do want to set the culture on VariationContext and PublishedRequest to the values specified by the domains. + /// + /// + /// + /// The request to update the culture on domain on + /// True if a domain was found otherwise false. + bool RouteDomain(IPublishedRequestBuilder request) => false; + + /// + /// Finds the site root (if any) matching the http request, and updates the VariationContext accordingly. + /// + /// + /// + /// This is used for VirtualPage routing. + /// + /// + /// This is required to set the culture on VariationContext to the values specified by the domains, before the FindContent method is called. + /// In order to allow the FindContent implementer to correctly find content based off the culture. Before the PublishedRequest is built. + /// + /// + /// The URI to resolve the domain from. + /// True if a domain was found, otherwise false. + bool UpdateVariationContext(Uri uri) => false; + } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index f04fd04ca2..df1d459327 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -108,7 +108,7 @@ public class PublishedRouter : IPublishedRouter // find domain if (builder.Domain == null) { - FindDomain(builder); + FindAndSetDomain(builder); } await RouteRequestInternalAsync(builder); @@ -185,7 +185,7 @@ public class PublishedRouter : IPublishedRouter private async Task TryRouteRequest(IPublishedRequestBuilder request) { - FindDomain(request); + FindAndSetDomain(request); if (request.IsRedirect()) { @@ -270,18 +270,31 @@ public class PublishedRouter : IPublishedRouter // to find out the appropriate template } - /// - /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a domain was found. - internal bool FindDomain(IPublishedRequestBuilder request) + /// + public bool RouteDomain(IPublishedRequestBuilder request) + { + var found = FindAndSetDomain(request); + HandleWildcardDomains(request); + SetVariationContext(request.Culture); + return found; + } + + /// + public bool UpdateVariationContext(Uri uri) + { + DomainAndUri? domain = FindDomain(uri, out _); + SetVariationContext(domain?.Culture); + return domain?.Culture is not null; + } + + private DomainAndUri? FindDomain(Uri uri, out string? defaultCulture) { const string tracePrefix = "FindDomain: "; // note - we are not handling schemes nor ports here. if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, uri); } IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); @@ -315,10 +328,20 @@ public class PublishedRouter : IPublishedRouter domains = domains?.Where(IsPublishedContentDomain).ToList(); - var defaultCulture = domainsCache?.DefaultCulture; + defaultCulture = domainsCache?.DefaultCulture; + return DomainUtilities.SelectDomain(domains, uri, defaultCulture: defaultCulture); + } + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindAndSetDomain(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindDomain: "; // try to find a domain matching the current request - DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + DomainAndUri? domainAndUri = FindDomain(request.Uri, out var defaultCulture); // handle domain - always has a contentId and a culture if (domainAndUri != null) diff --git a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs index 707dfe0b8d..fadd0d19e2 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; @@ -40,6 +42,12 @@ public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter if (endpoint != null) { IUmbracoVirtualPageRoute umbracoVirtualPageRoute = context.HttpContext.RequestServices.GetRequiredService(); + IPublishedRouter publishedRouter = context.HttpContext.RequestServices.GetRequiredService(); + UriUtility uriUtility = context.HttpContext.RequestServices.GetRequiredService(); + + var originalRequestUrl = new Uri(context.HttpContext.Request.GetEncodedUrl()); + Uri cleanedUri = uriUtility.UriToUmbraco(originalRequestUrl); + publishedRouter.UpdateVariationContext(cleanedUri); IPublishedContent? publishedContent = umbracoVirtualPageRoute.FindContent(endpoint, context); diff --git a/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs b/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs index cff5a589f6..9813cf3212 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs @@ -155,6 +155,7 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute IPublishedRequestBuilder requestBuilder = await _publishedRouter.CreateRequestAsync(cleanedUrl); requestBuilder.SetPublishedContent(publishedContent); + _publishedRouter.RouteDomain(requestBuilder); return requestBuilder.Build(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs index 3f639965cd..30bb4ae70c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -31,7 +31,7 @@ public class ContentFinderByAliasWithDomainsTests : UrlRoutingTestBase var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain - publishedRouter.FindDomain(request); + publishedRouter.FindAndSetDomain(request); if (expectedNode > 0) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs index c9069046ac..4606641265 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs @@ -207,7 +207,7 @@ public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain else lookup by URL fails - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); var result = await lookup.TryFindContent(frequest); @@ -245,7 +245,7 @@ public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain else lookup by URL fails - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture); var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs index c54e540864..3945e2346d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs @@ -261,7 +261,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture); @@ -310,7 +310,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); // find document var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); @@ -345,7 +345,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.IsNotNull(frequest.Domain); Assert.AreEqual(expectedCulture, frequest.Culture); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs index 9edce34707..d0536640e2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs @@ -62,7 +62,7 @@ public class UrlsWithNestedDomains : UrlRoutingTestBase var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.IsTrue(frequest.HasDomain()); // check that it's been routed From 24abc117fe5db9fda5dc2badea49ffcd9235971f Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 7 Jun 2024 12:50:46 +0200 Subject: [PATCH 38/61] update version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 288af47e08..b9fa941e03 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.5.0-rc", + "version": "13.4.0", "assemblyVersion": { "precision": "build" }, From 5e31fde4169d58a352ecc1b6cc7d2726d1dd52f8 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:49:16 +0200 Subject: [PATCH 39/61] V13 QA Updated depedencies (#16606) * Bumped version of helper and builder * Removed faker --- .../package-lock.json | 28 +++++++------------ .../Umbraco.Tests.AcceptanceTest/package.json | 5 ++-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 82892582ce..e3bb3947ff 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,11 +7,10 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", - "@umbraco/playwright-testhelpers": "^1.0.25", + "@umbraco/json-models-builders": "^1.0.8", + "@umbraco/playwright-testhelpers": "^1.0.28", "camelize": "^1.0.0", "dotenv": "^16.0.2", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1" @@ -122,22 +121,20 @@ "dev": true }, "node_modules/@umbraco/json-models-builders": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.6.tgz", - "integrity": "sha512-bXwfXcpuqG1Ye714L9KJEGXuSzJfckysE/6CuPjdG8FqHWTE1brv28teR2oMw+ih8ca2u2zUboRgdzLEU/1D3Q==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.8.tgz", + "integrity": "sha512-qBiOwaFO0V/hRpByBnwWe65nlNvaKNQPQj17MCiUTkCf1LxkB1T0ZoQv50vvnLFx6xUBZKfLhEXqYmoJqsQsPg==", "dependencies": { - "camelize": "^1.0.0", - "faker": "^4.1.0" + "camelize": "^1.0.0" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.25.tgz", - "integrity": "sha512-6H452J6LhP0EHjF4jR7V7i0U8WPTiAbSyhN1J459BbbYEJ4QX1A2ZlCdA6VSBAsK1xYdMXD+yxsVJq7AAwiy9A==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.28.tgz", + "integrity": "sha512-AOk0eKkGV1Tyhb+iac9hyKzm2wCqwf+ELRQsiCWNVW8DxrPZiXIqQyjeI22YFuzkVJ3MJBDcoSwp7d31b1gm/w==", "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", + "@umbraco/json-models-builders": "^1.0.8", "camelize": "^1.0.0", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1" @@ -323,11 +320,6 @@ "node": "> 0.1.90" } }, - "node_modules/faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" - }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index adcdf64f1d..6dc84f51dc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -19,10 +19,9 @@ "wait-on": "^7.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", - "@umbraco/playwright-testhelpers": "^1.0.25", + "@umbraco/json-models-builders": "^1.0.8", + "@umbraco/playwright-testhelpers": "^1.0.28", "camelize": "^1.0.0", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1", From 82b4f506c28909f6e66de050cf46589189ac89e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:08:40 +0200 Subject: [PATCH 40/61] bump version to rc2 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b9fa941e03..800c209374 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0", + "version": "13.4.0-rc2", "assemblyVersion": { "precision": "build" }, From 344245b1ea2af41ae43b04c1c54cbb9c899b844a Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:05:36 +0200 Subject: [PATCH 41/61] bump to final version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 800c209374..b9fa941e03 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0-rc2", + "version": "13.4.0", "assemblyVersion": { "precision": "build" }, From 8edb1885c96d4236614155e5da591f63f1fbd0af Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:40:59 +0200 Subject: [PATCH 42/61] Add missing translations (#16612) --- .../EmbeddedResources/Lang/bs.xml | 23 ++++++++++- .../EmbeddedResources/Lang/cs.xml | 10 ++++- .../EmbeddedResources/Lang/cy.xml | 16 +++++++- .../EmbeddedResources/Lang/en.xml | 25 +++++++++++- .../EmbeddedResources/Lang/en_us.xml | 23 +++++++++++ .../EmbeddedResources/Lang/es.xml | 4 +- .../EmbeddedResources/Lang/fr.xml | 10 ++++- .../EmbeddedResources/Lang/hr.xml | 23 ++++++++++- .../EmbeddedResources/Lang/it.xml | 39 ++++++++++++++++++- .../EmbeddedResources/Lang/nl.xml | 18 ++++++++- .../EmbeddedResources/Lang/pl.xml | 4 +- .../EmbeddedResources/Lang/ru.xml | 10 ++++- .../EmbeddedResources/Lang/tr.xml | 10 ++++- .../EmbeddedResources/Lang/ua.xml | 10 ++++- .../EmbeddedResources/Lang/zh_tw.xml | 4 +- 15 files changed, 215 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml index e72560cd5f..3e4e9ea08f 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml @@ -409,6 +409,27 @@ + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog je pronađen.]]> + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog nije pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga je pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga nije pronađen.]]> + + Strict-Transport-Security, takođe poznat kao HSTS-header, je pronađen.]]> + + Strict-Transport-Security nije pronađeno.]]> + + Strict-Transport-Security, takođe poznat kao HSTS-header, je pronađen. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + + Strict-Transport-Security nije pronađeno. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + X-XSS-Protection je pronađeno.]]> + + X-XSS-Protection nije pronađeno.]]> %0%.]]> Nisu pronađena zaglavlja koja otkrivaju informacije o tehnologiji web stranice. @@ -437,4 +458,4 @@ Pisanje fajlova Kreiranje medijskog foldera - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml index 8efd2506a2..e2f37a9ec7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml @@ -338,6 +338,14 @@ + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + X-Content-Type-Options použitá k ochraně před zranitelnostmi čichání MIME.]]> + X-Content-Type-Options použité k ochraně před zranitelnostmi čichání MIME nebyly nalezeny.]]> + Strict-Transport-Security, také známo jako HSTS-header, bylo nalezeno.]]> + Strict-Transport-Security nebylo nalezeno.]]> + X-XSS-Protection bylo nalezeno.]]> + X-XSS-Protection bylo nalezeno.]]> %0%.]]> Nebyly nalezeny žádné hlavičky odhalující informace o technologii webových stránek. Nastavení SMTP jsou správně nakonfigurována a služba funguje jak má. @@ -350,4 +358,4 @@ Obsah s ID: {0} v koši souvisí s původním nadřazeným obsahem s ID: {1} Média s ID: {0} v koši souvisí s původním nadřazeným médiem s ID: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index d0d67339bd..201f18059e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -420,6 +420,20 @@ + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + Strict-Transport-Security, hefyd wedi'i adnabod fel HSTS-header, wedi'i ganfod.]]> + Strict-Transport-Security wedi'i ganfod.]]> + + Strict-Transport-Security, a elwir hefyd yn HSTS-header. Ni ddylai'r pennyn hwn fod yn bresennol ar localhost.]]> + + + Strict-Transport-Security. Ni ddylai'r pennyn hwn fod yn bresennol ar localhost.]]> + + X-XSS-Protection wedi'i ganfod.]]> + X-XSS-Protection wedi'i ganfod.]]> %0%.]]> Dim peniadau sy'n datgelu gwynodaeth am dechnoleg eich gwefan wedi'u canfod. Gosodiadau SMTP wedi ffurfweddu'n gywir ac mae'r gwasanaeth yn gweithio fel y disgwylir. @@ -453,4 +467,4 @@
    • ID safle dienw, fersiwn Umbraco, a phecynnau wedi'u gosod.
    • Nifer o: Nodau gwraidd, Nodau Cynnwys, Macros, Cyfryngau, Mathau o Ddogfen, Templedi, Ieithoedd, Parthau, Grŵp Defnyddwyr, Defnyddwyr, Aelodau, a Golygyddion Eiddo a ddefnyddir.
    • Gwybodaeth system: Webserver, gweinydd OS, fframwaith gweinydd, iaith gweinyddwr OS, a darparwr cronfa ddata.
    • Gosodiadau cyfluniad: Modd Modelsbuilder, os oes llwybr Umbraco arferol yn bodoli, amgylchedd ASP, ac os ydych chi yn y modd dadfygio.
    Efallai y byddwn yn newid yr hyn a anfonwn ar y lefel Fanwl yn y dyfodol. Os felly, fe'i rhestrir uchod.
    Drwy ddewis "Manwl" rydych yn cytuno i wybodaeth ddienw yn awr ac yn y dyfodol gael ei chasglu.
    - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index aaccac8607..599232b13a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -442,6 +442,29 @@ + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + + Strict-Transport-Security was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found. This header should not be present on localhost.]]> + + + Strict-Transport-Security was not found. This header should not be present on localhost.]]> + + + X-XSS-Protection was found. It is recommended not to add this header to your website.
    + You can read about this on the Mozilla website ]]>
    + + X-XSS-Protection was not found.]]> %0%.]]> No headers revealing information about the website technology were found. @@ -483,4 +506,4 @@ We might change what we send on the Detailed level in the future. If so, it will be listed above.
    By choosing "Detailed" you agree to current and future anonymized information being collected.
    ]]> - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 01482e3863..7517a43b12 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -430,6 +430,29 @@ --> %0%.]]> The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + + Strict-Transport-Security was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found. This header should not be present on localhost.]]> + + + Strict-Transport-Security was not found. This header should not be present on localhost.]]> + + + X-XSS-Protection was found. It is recommended not to add this header to your website.
    + You can read about this on the Mozilla website ]]>
    + + X-XSS-Protection was not found.]]> %0%.]]> No headers revealing information about the website technology were found. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 7e82adb9c9..cfde5a04e7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -285,6 +285,8 @@ + X-Frame-Options usado para controlar si un sitio puede ser IFRAMEd por otra fue encontrado.]]> + X-Frame-Options usado para controlar si un sitio puede ser IFRAMEd por otra no se ha encontrado.]]> %0%.]]> No se ha encontrado ninguna cabecera que revele información sobre la tecnología del sitio. Los valores SMTP están configurados correctamente y el servicio opera con normalidad. @@ -293,4 +295,4 @@

    Los resultados de los Chequeos de Salud de Umbraco programados para ejecutarse el %0% a las %1% son:

    %2%]]>
    Status de los Chequeos de Salud de Umbraco: %0% - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml index 2edf4bae04..e8f07079ef 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml @@ -400,6 +400,14 @@ + X-Frame-Options, utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, a été trouvé.]]> + X-Frame-Options , utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, n'a pas été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing a été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing n'a pas été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, a été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, n'a pas été trouvé.]]> + X-XSS-Protection a été trouvé.]]> + X-XSS-Protection n'a pas été trouvé.]]> %0%.]]> Aucun header révélant des informations à propos de la technologie du site web n'a été trouvé. La configuration SMTP est correcte et le service fonctionne comme prévu. @@ -412,4 +420,4 @@ Suppression du contenu avec l'Id : {0} lié au contenu parent original avec l'Id : {1} Suppression du media avec l'Id : {0} lié à l'élément media parent original avec l'Id : {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml index eb64032a66..8ac145f3ce 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml @@ -407,6 +407,27 @@ + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog je pronađen.]]> + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog nije pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga je pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga nije pronađen.]]> + + Strict-Transport-Security, također poznat kao HSTS-header, je pronađen.]]> + + Strict-Transport-Security nije pronađeno.]]> + + Strict-Transport-Security, također poznat kao HSTS-header, je pronađen. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + + Strict-Transport-Security nije pronađeno. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + X-XSS-Protection je pronađeno.]]> + + X-XSS-Protection nije pronađeno.]]> %0%.]]> Nisu pronađena zaglavlja koja otkrivaju informacije o tehnologiji web stranice. @@ -450,4 +471,4 @@
    Odabirom "Detaljno" pristajete na prikupljanje trenutnih i budućih anonimiziranih informacija. ]]>
    - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml index 7fee004155..33cfd41534 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml @@ -397,6 +397,43 @@ + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro è stato trovato.]]> + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro non è stato trovato.]]> + Imposta l'header nella configurazione + Aggiunge un valore alla sezione httpProtocol/customHeaders del + file web.config per prevenire che un sito possa essere inserito in un IFRAME da altri siti web. + + + + Non posso aggiornare il file web.config. Errore: %0% + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + + + + + Strict-Transport-Security, conosciuto anche come l'header HSTS, è stato trovato.]]> + + Strict-Transport-Security non è stato trovato.]]> + Aggiunge l'header 'Strict-Transport-Security' con il valore + 'max-age=10886400' alla sezione httpProtocol/customHeaders del file web.config. Usa questa correzione solo se + avrai i tuoi domini in esecuzione con https per le prossime 18 settimane (minimo). + + + + X-XSS-Protection è stato trovato.]]> + + X-XSS-Protection non è stato trovato.]]> + Aggiunge l'header 'X-XSS-Protection' con il valore '1; + mode=block' alla sezione httpProtocol/customHeaders del file web.config. + + + %0%.]]> Non sono stati trovati header che rivelano informazioni riguardo alla tecnologia utilizzata per il sito. @@ -417,4 +454,4 @@ Media cestinato con Id: {0} relativo al media principale originale con Id: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index c067ea7e2e..2337343c2c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -279,6 +279,22 @@ + + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is aanwezig!]]> + + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is NIET aanwezig.]]> + + X-Content-Type-Options die beveiligt tegen MIME sniffing kwetsbaarheden is gevonden.]]> + + X-Content-Type-Options die beveiligt tegen MIME sniffing kwetsbaarheden is niet gevonden.]]> + + Strict-Transport-Security header, ook bekend als de HSTS-header, is gevonden.]]> + + Strict-Transport-Securityheader is niet gevonden.]]> + + X-XSS-Protection is gevonden.]]> + + X-XSS-Protection is niet gevonden.]]> %0%.]]> Er zijn geen headers gevonden die informatie vrijgeven over de gebruikte website technologie! @@ -294,4 +310,4 @@ Content verwijderd met id : {0} gerelateerd aan aan bovenliggend item met Id: {1} Media verwijderd met id: {0} gerelateerd aan aan bovenliggend item met Id: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml index ffce42770c..97b599df37 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml @@ -148,10 +148,12 @@ + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną został znaleziony.]]> + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną nie został znaleziony.]]> %0%.]]> Nie znaleziono żadnych nagłówków, ujawniających informacji o technologii strony. Ustawienia SMTP są skonfigurowane poprawnie i serwis działa według oczekiwań. %0%.]]> %0%.]]> - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml index 8b091b4017..7271ef2c6d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml @@ -91,6 +91,14 @@ + X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте.]]> + X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте, не обнаружен.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, обнаружены.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, не найдены.]]> + Strict-Transport-Security, известный также как HSTS-header, обнаружен.]]> + Strict-Transport-Security не найден.]]> + X-XSS-Protection обнаружен.]]> + X-XSS-Protection не найден.]]> %0%.]]> Заголовки, позволяющие выяснить базовую технологию сайта, не обнаружены. Параметры отправки электронной почты (SMTP) настроены корректно, сервис работает как ожидается. @@ -379,4 +387,4 @@ неверный формат email-адреса - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml index af1fe7db2f..90803e72a7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml @@ -389,6 +389,14 @@ + ​​ X-Frame-Options .]]> + X-Frame-Options bulunamadı.]]> + ​​ X-Content-Type-Options bulundu.]]> + ​​ X-Content-Type-Options bulunamadı.]]> + ​​ Strict-Transport-Security başlığı bulundu.]]> + ​​ Strict-Transport-Security başlığı bulunamadı.]]> + ​​ X-XSS-Protection başlığı bulundu.]]> + ​​ X-XSS-Protection başlığı bulunamadı.]]> ​​%0%.]]> ​​Web sitesi teknolojisi hakkında bilgi veren hiçbir başlık bulunamadı. SMTP ayarları doğru yapılandırıldı ve hizmet beklendiği gibi çalışıyor. @@ -401,4 +409,4 @@ Çöp kutusuna gönderilmiş içerik: {0} Şu kimliğe sahip orijinal ana içerikle ilgili: {1} Şu kimliğe sahip çöp kutusuna gönderilen medya: {0} Şu kimliğe sahip orijinal ana medya öğesiyle ilgili: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml index 0ca656292f..047ea953de 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml @@ -91,6 +91,14 @@ + X-Frame-Options, що використовується для управління можливістю розміщувати сайт у IFRAME на іншому сайті, знайдено.]]> + X-Frame-Options, що використовується для управління можливістю розміщувати сайт у IFRAME на іншому сайті, не знайдено.]]> + X-Content-Type-Options, що використовується для захисту від MIME-уязвимостей, знайдено.]]> + X-Content-Type-Options, що використовується для захисту від MIME-уязвимостей, не знайдено.]]> + Strict-Transport-Security, відомий також як HSTS-header, знайдено.]]> + Strict-Transport-Security не знайдено.]]> + X-XSS-Protection знайдено.]]> + X-XSS-Protection не знайдено.]]> %0%.]]> Заголовки, які дають змогу з'ясувати базову технологію сайту, не виявлено. Параметри надсилання електронної пошти (SMTP) налаштовані коректно, сервіс працює як очікується. @@ -379,4 +387,4 @@ Невірний формат email-адреси - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml index 00667df60c..4c5e7c9dbc 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml @@ -137,10 +137,12 @@ + X-Frame-Options 設定能控制網站是否可以被其他人IFRAMEd已找到。]]> + X-Frame-Options 設定能控制網站是否可以被其他人IFRAMEd沒有找到。]]> %0%。]]> 在標頭中沒有找到揭露網站技術的資訊。 SMTP設定正確,而且服務正常運作。 %0%。]]> %0%。]]> - \ No newline at end of file + From 01909ca20440d7a0dd769142a4e864aa65b06630 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:36:00 +0200 Subject: [PATCH 43/61] V14 QA Users acceptance tests (#15916) * Added tests for users * Additional tests * Found additional test scenarios * File select * Fixed tests * Cleaned up user tests * Bumped versions of helpers and builder * Updated naming of method * More updates * Added more tests * Fixed tests * Fixed skipped tests * Final touches * Bumped * Run userTests * Fixes made based on review * Updated so we run our smoketest again --- .../package-lock.json | 18 +- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- .../tests/DefaultConfig/Users/User.spec.ts | 577 ++++++++++++++++++ 3 files changed, 588 insertions(+), 11 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 940e5ca79c..8383cb5028 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,8 +7,8 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.6", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.60", + "@umbraco/json-models-builders": "^2.0.7", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.61", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", @@ -132,9 +132,9 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.6.tgz", - "integrity": "sha512-eoOhTSH7rcC7NESId0vhqtxNXPuoy+ZaQo1moXxpv8/T6vqmKoDdLEydjtDz0FOXzqVZ5yQ1xWK0cpag37Laag==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.7.tgz", + "integrity": "sha512-roR5A+jzIFN9z1BhogMGOEzSzoR8jOrIYIAevT7EnyS3H3OM0m0uREgvjYCQo0+QMfVws4zq4Ydjx2TIfGYvlQ==", "dependencies": { "camelize": "^1.0.1", "faker": "^6.6.6" @@ -146,11 +146,11 @@ "integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==" }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.60", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.60.tgz", - "integrity": "sha512-5KJkn1GtfCXqbwYP8RnDyjWUNqSQ/62UYFARuXhUzQoz4xvv3Fme8rPeiOBxqJRWWoj3MQCaP7nyPPs3FDe8vQ==", + "version": "2.0.0-beta.61", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.61.tgz", + "integrity": "sha512-Y2RqGrjfLDCZGDPyix4r8LoSl/YaluzY8RHLlkdcbL5GojDprzfB0jN9P3ZbrCDvnu9hydA8qE6ElPq/Zw5qXw==", "dependencies": { - "@umbraco/json-models-builders": "2.0.6", + "@umbraco/json-models-builders": "2.0.7", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index c7261b06f3..11606d1a8e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,8 +21,8 @@ "wait-on": "^7.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.6", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.60", + "@umbraco/json-models-builders": "^2.0.7", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.61", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts new file mode 100644 index 0000000000..789bf96939 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -0,0 +1,577 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from '@playwright/test'; + +const nameOfTheUser = 'TestUser'; +const userEmail = 'TestUser@EmailTest.test'; +const defaultUserGroupName = 'Writers'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoUi.goToBackOffice(); + await umbracoApi.user.ensureNameNotExists(nameOfTheUser); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.user.ensureNameNotExists(nameOfTheUser); +}); + +test('can create a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickCreateButton(); + await umbracoUi.user.enterNameOfTheUser(nameOfTheUser); + await umbracoUi.user.enterUserEmail(userEmail); + await umbracoUi.user.clickChooseButton(); + await umbracoUi.user.clickButtonWithName(defaultUserGroupName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickCreateUserButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeTruthy(); +}); + +test('can rename a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'WrongName'; + await umbracoApi.user.ensureNameNotExists(wrongName); + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(wrongName, wrongName + userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(wrongName); + await umbracoUi.user.enterUpdatedNameOfUser(nameOfTheUser); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeTruthy(); +}); + +test('can delete a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickDeleteButton(); + await umbracoUi.user.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeFalsy(); + // Checks if the user is deleted from the list + await umbracoUi.user.clickUsersTabButton(); + await umbracoUi.user.isUserVisible(nameOfTheUser, false); +}); + +test('can add multiple user groups to a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const secondUserGroupName = 'Translators'; + const userGroupWriters = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userGroupTranslators = await umbracoApi.userGroup.getByName(secondUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroupWriters.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseUserGroupsButton(); + await umbracoUi.user.clickButtonWithName(secondUserGroupName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(await umbracoApi.user.doesUserContainUserGroupIds(nameOfTheUser, [userGroupWriters.id, userGroupTranslators.id])).toBeTruthy(); +}); + +test('can remove a user group from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForUserGroupWithName(defaultUserGroupName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.userGroupIds).toEqual([]); +}); + +test('can update culture for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const danishIsoCode = 'da-dk'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.selectUserLanguage('Dansk'); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.languageIsoCode).toEqual(danishIsoCode); +}); + +test('can add a content start node to a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseContentStartNodeButton(); + await umbracoUi.user.clickLabelWithName(documentName); + await umbracoUi.user.clickChooseContainerButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add multiple content start nodes for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + const secondDocumentName = 'SecondDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.document.ensureNameNotExists(secondDocumentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + const secondDocumentId = await umbracoApi.document.createDefaultDocument(secondDocumentName, documentTypeId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseContentStartNodeButton(); + await umbracoUi.user.clickLabelWithName(secondDocumentName); + await umbracoUi.user.clickChooseContainerButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId, secondDocumentId])).toBeTruthy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.document.ensureNameNotExists(secondDocumentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForContentNodeWithName(documentName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeFalsy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const mediaName = 'TestMediaFile'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.clickMediaCardWithName(mediaName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can add multiple media start nodes for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + const secondMediaName = 'SecondMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(secondMediaName); + const firstMediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const secondMediaId = await umbracoApi.media.createDefaultMedia(secondMediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: firstMediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.clickMediaCardWithName(secondMediaName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId, secondMediaId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(secondMediaName); +}); + +test('can remove a media start node from a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: mediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForMediaNodeWithName(mediaName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can allow access to all documents for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickAllowAccessToAllDocumentsSlider(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.hasDocumentRootAccess).toBeTruthy() +}); + +test('can allow access to all media for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickAllowAccessToAllMediaSlider(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.hasMediaRootAccess).toBeTruthy(); +}); + +test('can see if the user has the correct access based on content start nodes', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + + // Assert + await umbracoUi.user.doesUserHaveAccessToContentNode(documentName); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can see if the user has the correct access based on media start nodes', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: mediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + + // Assert + await umbracoUi.user.doesUserHaveAccessToMediaNode(mediaName); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can change password for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userPassword = 'TestPassword'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChangePasswordButton(); + await umbracoUi.user.updatePassword(userPassword); + + // Assert + await umbracoUi.user.isPasswordUpdatedForUserWithId(userId); +}); + +test('can disable a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const disabledStatus = 'Disabled'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickDisableButton(); + await umbracoUi.user.clickConfirmDisableButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(umbracoUi.user.isUserDisabledTextVisible()).toBeTruthy(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.state).toBe(disabledStatus); +}); + +test('can enable a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const inactiveStatus = 'Inactive'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.user.disable([userId]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickEnableButton(); + await umbracoUi.user.clickConfirmEnableButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + await umbracoUi.user.isUserActiveTextVisible(); + // The state of the user is not enabled. The reason for this is that the user has not logged in, resulting in the state Inactive. + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.state).toBe(inactiveStatus); +}); + +test('can add an avatar to a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const filePath = './fixtures/mediaLibrary/Umbraco.png'; + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.changePhotoWithFileChooser(filePath); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.avatarUrls).not.toHaveLength(0); +}); + +test('can remove an avatar from a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.user.addDefaultAvatarImageToUser(userId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemovePhotoButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.avatarUrls).toHaveLength(0); +}); + +test('can see if the inactive label is removed from the admin user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userLabel = 'Active'; + const currentUser = await umbracoApi.user.getCurrentUser(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(currentUser.name); + + // Assert + await umbracoUi.user.isTextWithExactNameVisible(userLabel); + const userData = await umbracoApi.user.getByName(currentUser.name); + expect(userData.state).toBe(userLabel); +}); + +test('can search for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.searchInUserSection(nameOfTheUser); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); +}); + +test('can filter by status', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inactiveStatus = 'Inactive'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.filterByStatusName(inactiveStatus); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); + await umbracoUi.user.doesUserSectionContainUserWithText(inactiveStatus); +}); + +test('can filter by user groups', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.filterByGroupName(defaultUserGroupName); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(defaultUserGroupName); +}); + +test('can order by newest user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.orderByNewestUser(); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.isUserWithNameTheFirstUserInList(nameOfTheUser); +}); + +// TODO: Sometimes the frontend does not switch from grid to table, or table to grid. +test.skip('can change from grid to table view', async ({page, umbracoApi, umbracoUi}) => { +}); From a686ba2a0ed97cb0f818e532bc84dbd179c4c027 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:50:52 +0200 Subject: [PATCH 44/61] V13: Update nuget packages (#16616) * Update nuget packages * Fix Imagesharp 2 --------- Co-authored-by: Bjarke Berg --- Directory.Packages.props | 48 +++++++++---------- .../Umbraco.Cms.Imaging.ImageSharp.csproj | 2 +- .../Umbraco.Cms.Imaging.ImageSharp2.csproj | 4 +- .../Umbraco.PublishedCache.NuCache.csproj | 4 +- .../Umbraco.Web.Common.csproj | 2 +- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 02dbaba948..ccd1e91a2e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,25 +12,25 @@ - - + + - - - - + + + + - + - - + + - + @@ -45,13 +45,13 @@ - - - - - + + + + + - + @@ -73,21 +73,21 @@ - - - - - + + + + + - + - + - + \ No newline at end of file diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index 13126a24b5..549ea5cb40 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -6,7 +6,7 @@ - + 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 43bf47cb75..724c5b5e34 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -5,10 +5,8 @@ - + - - diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index 75dfa13ec6..92f222525c 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -7,10 +7,10 @@ - + - + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 02a039a6db..b0b5f2af6a 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -12,7 +12,7 @@ - + From 65c76fcfab17d7a66966c3f5a0a5576ef91fbfaa Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:49:04 +0200 Subject: [PATCH 45/61] Decreased to 1 retry for this file only (#16623) --- .../tests/DefaultConfig/Tour/tours.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts index f5974db43d..03bbe86c78 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts @@ -3,6 +3,7 @@ import {test} from '@umbraco/playwright-testhelpers'; test.describe('Tours', () => { const timeout = 60000; + test.describe.configure({ retries: 1 }); test.beforeEach(async ({ page, umbracoApi }, testInfo) => { await umbracoApi.report.report(testInfo); await umbracoApi.login(); @@ -100,4 +101,4 @@ test.describe('Tours', () => { await expect(await umbracoUi.getGlobalHelp()).toBeVisible(); await getPercentage(17, timeout, page); }); -}); \ No newline at end of file +}); From 1f52d01493d502b139f135a640bd0b80c23f5eaf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 19 Jun 2024 15:19:31 +0200 Subject: [PATCH 46/61] Do not rely on claims to figure out user access + prepare for claims removal (#16552) * Do not rely on claims to figure out user access + prepare for claims removal * Fix typos :) * Ensure we fire all relevant notifications when creating and updating user groups * Leave it to the cache refreshers to flush user cache data (start nodes) --- .../BackOfficeAuthBuilderExtensions.cs | 3 - .../BackOfficeAuthPolicyBuilderExtensions.cs | 63 ++++--- ...AuthenticationTokensNotificationHandler.cs | 160 ------------------ .../User/AllowedApplicationHandler.cs | 24 +++ .../User/AllowedApplicationRequirement.cs | 14 ++ src/Umbraco.Core/Constants-Security.cs | 3 + .../Extensions/ClaimsIdentityExtensions.cs | 8 +- src/Umbraco.Core/Models/UserExtensions.cs | 24 +-- src/Umbraco.Core/Services/UserGroupService.cs | 22 ++- 9 files changed, 112 insertions(+), 209 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index e36f103e92..5ddb98b570 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -27,11 +27,8 @@ public static class BackOfficeAuthBuilderExtensions public static IUmbracoBuilder AddTokenRevocation(this IUmbracoBuilder builder) { - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); return builder; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 4714c54c74..45eccad5ec 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -28,6 +28,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddAuthorization(CreatePolicies); return builder; @@ -35,14 +36,12 @@ internal static class BackOfficeAuthPolicyBuilderExtensions private static void CreatePolicies(AuthorizationOptions options) { - void AddPolicy(string policyName, string claimType, params string[] allowedClaimValues) - { - options.AddPolicy(policyName, policy => + void AddAllowedApplicationsPolicy(string policyName, params string[] allowedClaimValues) + => options.AddPolicy(policyName, policy => { policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - policy.RequireClaim(claimType, allowedClaimValues); + policy.Requirements.Add(new AllowedApplicationRequirement(allowedClaimValues)); }); - } options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { @@ -56,39 +55,39 @@ internal static class BackOfficeAuthPolicyBuilderExtensions policy.RequireRole(Constants.Security.AdminGroupAlias); }); - AddPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); - AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Media); - AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media); - AddPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Packages); - AddPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Users); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Applications.Users); - AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation); - AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Applications.Translation); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Applications.Media, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Applications.Settings); // Contextual permissions options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => diff --git a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs index acf6cb508a..e56e776f85 100644 --- a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs @@ -13,77 +13,31 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Handlers; internal sealed class RevokeUserAuthenticationTokensNotificationHandler : - INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, - INotificationAsyncHandler, - INotificationAsyncHandler, INotificationAsyncHandler { - private const string NotificationStateKey = "Umbraco.Cms.Api.Management.Handlers.RevokeUserAuthenticationTokensNotificationHandler"; - private readonly IUserService _userService; - private readonly IUserGroupService _userGroupService; private readonly IOpenIddictTokenManager _tokenManager; private readonly ILogger _logger; private readonly SecuritySettings _securitySettings; public RevokeUserAuthenticationTokensNotificationHandler( IUserService userService, - IUserGroupService userGroupService, IOpenIddictTokenManager tokenManager, ILogger logger, IOptions securitySettingsOptions) { _userService = userService; - _userGroupService = userGroupService; _tokenManager = tokenManager; _logger = logger; _securitySettings = securitySettingsOptions.Value; } - // We need to know the pre-saving state of the saved users in order to compare if their access has changed - public async Task HandleAsync(UserSavingNotification notification, CancellationToken cancellationToken) - { - try - { - var usersAccess = new Dictionary(); - foreach (IUser user in notification.SavedEntities) - { - UserStartNodesAndGroupAccess? priorUserAccess = await GetRelevantUserAccessDataByUserKeyAsync(user.Key); - if (priorUserAccess == null) - { - continue; - } - - usersAccess.Add(user.Key, priorUserAccess); - } - - notification.State[NotificationStateKey] = usersAccess; - } - catch (DbException e) - { - _logger.LogWarning(e, "This is expected when we upgrade from < Umbraco 14. Otherwise it should not happen"); - } - } - public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken) { try { - Dictionary? preSavingUsersState = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preSavingUsersState = value as Dictionary; - } - - // If we have a new user, there is no token - if (preSavingUsersState is null || preSavingUsersState.Count == 0) - { - return; - } - foreach (IUser user in notification.SavedEntities) { if (user.IsSuper()) @@ -95,23 +49,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : if (user.IsLockedOut || user.IsApproved is false) { await RevokeTokensAsync(user); - continue; - } - - // Don't revoke admin tokens to prevent log out when accidental changes - if (user.IsAdmin()) - { - continue; - } - - // Check if the user access has changed - we also need to revoke all tokens in this case - if (preSavingUsersState.TryGetValue(user.Key, out UserStartNodesAndGroupAccess? preSavingState)) - { - UserStartNodesAndGroupAccess postSavingState = MapToUserStartNodesAndGroupAccess(user); - if (preSavingState.CompareAccess(postSavingState) == false) - { - await RevokeTokensAsync(user); - } } } } @@ -131,49 +68,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // We need to know the pre-deleting state of the users part of the deleted group to revoke their tokens - public async Task HandleAsync(UserGroupDeletingNotification notification, CancellationToken cancellationToken) - { - var usersInGroups = new Dictionary>(); - foreach (IUserGroup userGroup in notification.DeletedEntities) - { - var users = await GetUsersByGroupKeyAsync(userGroup.Key); - if (users == null) - { - continue; - } - - usersInGroups.Add(userGroup.Key, users); - } - - notification.State[NotificationStateKey] = usersInGroups; - } - - public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) - { - Dictionary>? preDeletingUsersInGroups = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preDeletingUsersInGroups = value as Dictionary>; - } - - if (preDeletingUsersInGroups is null) - { - return; - } - - // since the user group was deleted, we can only use the information we collected before the deletion - // this means that we will not be able to detect users in any groups that were eventually deleted (due to implementor/3th party supplier interference) - // that were not in the initial to be deleted list - foreach (IUser user in preDeletingUsersInGroups - .Where(group => notification.DeletedEntities.Any(entity => group.Key == entity.Key)) - .SelectMany(group => group.Value)) - { - await RevokeTokensAsync(user); - } - } - public async Task HandleAsync(UserLoginSuccessNotification notification, CancellationToken cancellationToken) { if (_securitySettings.AllowConcurrentLogins is false) @@ -190,29 +84,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // Get data about the user before saving - private async Task GetRelevantUserAccessDataByUserKeyAsync(Guid userKey) - { - IUser? user = await _userService.GetAsync(userKey); - - return user is null - ? null - : MapToUserStartNodesAndGroupAccess(user); - } - - private UserStartNodesAndGroupAccess MapToUserStartNodesAndGroupAccess(IUser user) - => new(user.Groups.Select(g => g.Key), user.StartContentIds, user.StartMediaIds); - - // Get data about the users part of a group before deleting it - private async Task?> GetUsersByGroupKeyAsync(Guid userGroupKey) - { - IUserGroup? userGroup = await _userGroupService.GetAsync(userGroupKey); - - return userGroup is null - ? null - : _userService.GetAllInGroup(userGroup.Id); - } - private async Task RevokeTokensAsync(IUser user) { _logger.LogInformation("Revoking active tokens for user with ID {id}", user.Id); @@ -236,35 +107,4 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : return null; } - - private class UserStartNodesAndGroupAccess - { - public IEnumerable GroupKeys { get; } - - public int[]? StartContentIds { get; } - - public int[]? StartMediaIds { get; } - - public UserStartNodesAndGroupAccess(IEnumerable groupKeys, int[]? startContentIds, int[]? startMediaIds) - { - GroupKeys = groupKeys; - StartContentIds = startContentIds; - StartMediaIds = startMediaIds; - } - - public bool CompareAccess(UserStartNodesAndGroupAccess other) - { - var areContentStartNodesEqual = (StartContentIds == null && other.StartContentIds == null) || - (StartContentIds != null && other.StartContentIds != null && - StartContentIds.SequenceEqual(other.StartContentIds)); - - var areMediaStartNodesEqual = (StartMediaIds == null && other.StartMediaIds == null) || - (StartMediaIds != null && other.StartMediaIds != null && - StartMediaIds.SequenceEqual(other.StartMediaIds)); - - return areContentStartNodesEqual && - areMediaStartNodesEqual && - GroupKeys.SequenceEqual(other.GroupKeys); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs new file mode 100644 index 0000000000..a36a592827 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorizes that the current user has the correct permission access to the applications listed in the requirement. +/// +internal sealed class AllowedApplicationHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IAuthorizationHelper _authorizationHelper; + + public AllowedApplicationHandler(IAuthorizationHelper authorizationHelper) + => _authorizationHelper = authorizationHelper; + + protected override Task IsAuthorized(AuthorizationHandlerContext context, AllowedApplicationRequirement requirement) + { + IUser user = _authorizationHelper.GetUmbracoUser(context.User); + var allowed = user.AllowedSections.ContainsAny(requirement.Applications); + return Task.FromResult(allowed); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs new file mode 100644 index 0000000000..dce6d8773e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +internal sealed class AllowedApplicationRequirement : IAuthorizationRequirement +{ + public string[] Applications { get; } + + public AllowedApplicationRequirement(params string[] applications) + => Applications = applications; +} diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 842add06fa..2f24fa182d 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -93,12 +93,15 @@ public static partial class Constants public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + [Obsolete("Please use IUser.AllowedSections instead. Will be removed in V15.")] public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 38f9bf15ff..73157767b2 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -34,8 +34,6 @@ public static class ClaimsIdentityExtensions ClaimTypes.Name, // username ClaimTypes.GivenName, - // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... - // Constants.Security.StartMediaNodeIdClaimType, ClaimTypes.Locality, Constants.Security.SecurityStampClaimType, }; @@ -250,6 +248,7 @@ public static class ClaimsIdentityExtensions identity)); } + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null) { @@ -265,6 +264,7 @@ public static class ClaimsIdentityExtensions } } + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null) { @@ -304,6 +304,7 @@ public static class ClaimsIdentityExtensions } // Add each app as a separate claim + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) { foreach (var application in allowedApps) @@ -343,6 +344,7 @@ public static class ClaimsIdentityExtensions ///
    /// /// Array of start content nodes + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public static int[] GetStartContentNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) @@ -355,6 +357,7 @@ public static class ClaimsIdentityExtensions ///
    /// /// Array of start media nodes + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) @@ -367,6 +370,7 @@ public static class ClaimsIdentityExtensions ///
    /// /// + [Obsolete("Please use IUser.AllowedSections instead. Will be removed in V15.")] public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 6682e6e055..b3c115c41b 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Security.Cryptography; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; @@ -155,8 +153,8 @@ public static class UserExtensions public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Key; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserAllContentStartNodesPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -189,8 +187,8 @@ public static class UserExtensions /// public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserAllMediaStartNodesPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -214,8 +212,8 @@ public static class UserExtensions public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserMediaStartNodePathsPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -232,8 +230,8 @@ public static class UserExtensions public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserContentStartNodePathsPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -317,6 +315,12 @@ public static class UserExtensions return lsn.ToArray(); } + private static IAppPolicyCache GetUserCache(AppCaches appCaches) + => appCaches.IsolatedCaches.GetOrCreate(); + + private static string UserCacheKey(this IUser user, string cacheKey) + => $"{cacheKey}{user.Key}"; + private static bool StartsWithPath(string test, string path) => test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; diff --git a/src/Umbraco.Core/Services/UserGroupService.cs b/src/Umbraco.Core/Services/UserGroupService.cs index 56c2222b3c..fbe6a52386 100644 --- a/src/Umbraco.Core/Services/UserGroupService.cs +++ b/src/Umbraco.Core/Services/UserGroupService.cs @@ -314,8 +314,7 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService // Since this is a brand new creation we don't have to be worried about what users were added and removed // simply put all members that are requested to be in the group will be "added" var userGroupWithUsers = new UserGroupWithUsers(userGroup, usersToAdd, Array.Empty()); - var savingUserGroupWithUsersNotification = - new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); + var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification)) { scope.Complete(); @@ -324,6 +323,11 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, usersToAdd.Select(x => x.Id).ToArray()); + scope.Notifications.Publish( + new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, eventMessages).WithStateFrom(savingUserGroupWithUsersNotification)); + scope.Complete(); return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup); } @@ -385,9 +389,23 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup); } + // We need to fire this notification - both for backwards compat, and to ensure caches across all servers. + // Since we are not adding or removing any users, we'll just fire the notification with empty collections + // for "added" and "removed" users. + var userGroupWithUsers = new UserGroupWithUsers(userGroup, [], []); + var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup); + } + _userGroupRepository.Save(userGroup); + scope.Notifications.Publish( new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, eventMessages).WithStateFrom(savingUserGroupWithUsersNotification)); scope.Complete(); return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup); From 75c42f4ea4e365f7d19f3244e851f94845cc01d6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 19 Jun 2024 15:21:57 +0200 Subject: [PATCH 47/61] Added post configuration of OpenIddictServerOptions that removes the ValidateTransportSecurityRequirement iff globalsettings.usehttps is false. (#16614) --- .../Configuration/PostConfigureOpenIddict.cs | 44 +++++++++++++++++++ .../UmbracoBuilderAuthExtensions.cs | 2 + 2 files changed, 46 insertions(+) create mode 100644 src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs diff --git a/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs b/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs new file mode 100644 index 0000000000..f01b71fbb1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Api.Common.Configuration; + +internal class PostConfigureOpenIddict : IPostConfigureOptions +{ + private readonly IOptions _globalSettings; + + public PostConfigureOpenIddict(IOptions globalSettings) + { + _globalSettings = globalSettings; + } + + public void PostConfigure(string? name, OpenIddictServerOptions options) + { + EnsureHttpsIsNotRequiredWhenConfigAllowHttp(options); + } + + /// + /// Ensures OpenIddict is configured to allow Http requrest, if and only if, the global settings are configured to allow Http. + /// + /// + /// The logic actually allowing http by removing the ValidateTransportSecurityRequirement Descriptor is borrowed from + /// + private void EnsureHttpsIsNotRequiredWhenConfigAllowHttp(OpenIddictServerOptions options) + { + if (_globalSettings.Value.UseHttps is false) + { + OpenIddictServerHandlerDescriptor descriptor = OpenIddictServerAspNetCoreHandlers.ValidateTransportSecurityRequirement.Descriptor; + + for (var index = options.Handlers.Count - 1; index >= 0; index--) + { + if (options.Handlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType) + { + options.Handlers.RemoveAt(index); + } + } + } + } +} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 7e730695f3..c215eeecf8 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; using OpenIddict.Validation; +using Umbraco.Cms.Api.Common.Configuration; using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -132,5 +133,6 @@ public static class UmbracoBuilderAuthExtensions }); builder.Services.AddRecurringBackgroundJob(); + builder.Services.ConfigureOptions(); } } From f717a5d0b7646dd72397101c2ddf01e6b7db5784 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 20 Jun 2024 10:39:24 +0200 Subject: [PATCH 48/61] Simplified how we disable the TransportSecurityRequirement in OpenIddict (#16629) --- .../Configuration/ConfigureOpenIddict.cs | 15 +++++++ .../Configuration/PostConfigureOpenIddict.cs | 44 ------------------- .../UmbracoBuilderAuthExtensions.cs | 2 +- 3 files changed, 16 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs delete mode 100644 src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs new file mode 100644 index 0000000000..f428957bd9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Api.Common.Configuration; + +internal class ConfigureOpenIddict : IConfigureOptions +{ + private readonly IOptions _globalSettings; + + public ConfigureOpenIddict(IOptions globalSettings) => _globalSettings = globalSettings; + + public void Configure(OpenIddictServerAspNetCoreOptions options) + => options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false; +} diff --git a/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs b/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs deleted file mode 100644 index f01b71fbb1..0000000000 --- a/src/Umbraco.Cms.Api.Common/Configuration/PostConfigureOpenIddict.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenIddict.Server; -using OpenIddict.Server.AspNetCore; -using Umbraco.Cms.Core.Configuration.Models; - -namespace Umbraco.Cms.Api.Common.Configuration; - -internal class PostConfigureOpenIddict : IPostConfigureOptions -{ - private readonly IOptions _globalSettings; - - public PostConfigureOpenIddict(IOptions globalSettings) - { - _globalSettings = globalSettings; - } - - public void PostConfigure(string? name, OpenIddictServerOptions options) - { - EnsureHttpsIsNotRequiredWhenConfigAllowHttp(options); - } - - /// - /// Ensures OpenIddict is configured to allow Http requrest, if and only if, the global settings are configured to allow Http. - /// - /// - /// The logic actually allowing http by removing the ValidateTransportSecurityRequirement Descriptor is borrowed from - /// - private void EnsureHttpsIsNotRequiredWhenConfigAllowHttp(OpenIddictServerOptions options) - { - if (_globalSettings.Value.UseHttps is false) - { - OpenIddictServerHandlerDescriptor descriptor = OpenIddictServerAspNetCoreHandlers.ValidateTransportSecurityRequirement.Descriptor; - - for (var index = options.Handlers.Count - 1; index >= 0; index--) - { - if (options.Handlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType) - { - options.Handlers.RemoveAt(index); - } - } - } - } -} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index c215eeecf8..3619da3071 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -133,6 +133,6 @@ public static class UmbracoBuilderAuthExtensions }); builder.Services.AddRecurringBackgroundJob(); - builder.Services.ConfigureOptions(); + builder.Services.ConfigureOptions(); } } From 00ca9e0d2dbfb9c8a75fd8cfc00ca272ae18efbe Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 20 Jun 2024 11:25:02 +0200 Subject: [PATCH 49/61] V13: Eaglery route domains for virtual page controllers (#16635) * Do domain routing eagerly * Cleanup * Fix comment --- .../Routing/EagerMatcherPolicy.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index bb5cb52a4d..b6cd4a3615 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -8,7 +8,9 @@ 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.Website.Controllers; using Umbraco.Extensions; @@ -37,6 +39,8 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy private readonly IRuntimeState _runtimeState; private readonly EndpointDataSource _endpointDataSource; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedRouter _publishedRouter; private GlobalSettings _globalSettings; private readonly Lazy _installEndpoint; private readonly Lazy _renderEndpoint; @@ -45,11 +49,15 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy IRuntimeState runtimeState, EndpointDataSource endpointDataSource, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor globalSettings) + IOptionsMonitor globalSettings, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter) { _runtimeState = runtimeState; _endpointDataSource = endpointDataSource; _umbracoRequestPaths = umbracoRequestPaths; + _umbracoContextAccessor = umbracoContextAccessor; + _publishedRouter = publishedRouter; _globalSettings = globalSettings.CurrentValue; globalSettings.OnChange(settings => _globalSettings = settings); _installEndpoint = new Lazy(GetInstallEndpoint); @@ -112,11 +120,22 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy ControllerActionDescriptor? controllerDescriptor = routeEndpoint.Metadata.GetMetadata(); TypeInfo? controllerTypeInfo = controllerDescriptor?.ControllerTypeInfo; if (controllerTypeInfo is not null && - (controllerTypeInfo.IsType() || controllerTypeInfo.IsType())) + (controllerTypeInfo.IsType() + || controllerTypeInfo.IsType())) { return; } + // If it's an UmbracoPageController we need to do some domain routing. + // We need to do this in oder to handle cultures for our Dictionary. + // This is because UmbracoPublishedContentCultureProvider is ued to set the Thread.CurrentThread.CurrentUICulture + // The CultureProvider is run before the actual routing, this means that our UmbracoVirtualPageFilterAttribute is hit AFTER the culture is set. + // Meaning we have to route the domain part already now, this is not pretty, but it beats having to look for content we know doesn't exist. + if (controllerTypeInfo is not null && controllerTypeInfo.IsType()) + { + await RouteVirtualRequestAsync(httpContext); + } + if (routeEndpoint.Order < lowestOrder) { // We have to ensure that the route is valid for the current request method. @@ -153,6 +172,22 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } } + private async Task RouteVirtualRequestAsync(HttpContext context) + { + if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) is false) + { + return; + } + + IPublishedRequestBuilder requestBuilder = + await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + _publishedRouter.RouteDomain(requestBuilder); + // This is just a temporary RouteValues object just for culture which will be overwritten later + // so we can just use a dummy action descriptor. + var umbracoRouteValues = new UmbracoRouteValues(requestBuilder.Build(), new ControllerActionDescriptor()); + context.Features.Set(umbracoRouteValues); + } + /// /// Replaces the first endpoint candidate with the specified endpoint, invalidating all other candidates, /// guaranteeing that the specified endpoint will be hit. From 56710d5b5be2ecc2d09bce04168b2fdd149d7341 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 20 Jun 2024 11:25:02 +0200 Subject: [PATCH 50/61] V13: Eaglery route domains for virtual page controllers (#16635) * Do domain routing eagerly * Cleanup * Fix comment --- .../Routing/EagerMatcherPolicy.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index bb5cb52a4d..b6cd4a3615 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -8,7 +8,9 @@ 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.Website.Controllers; using Umbraco.Extensions; @@ -37,6 +39,8 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy private readonly IRuntimeState _runtimeState; private readonly EndpointDataSource _endpointDataSource; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedRouter _publishedRouter; private GlobalSettings _globalSettings; private readonly Lazy _installEndpoint; private readonly Lazy _renderEndpoint; @@ -45,11 +49,15 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy IRuntimeState runtimeState, EndpointDataSource endpointDataSource, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor globalSettings) + IOptionsMonitor globalSettings, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter) { _runtimeState = runtimeState; _endpointDataSource = endpointDataSource; _umbracoRequestPaths = umbracoRequestPaths; + _umbracoContextAccessor = umbracoContextAccessor; + _publishedRouter = publishedRouter; _globalSettings = globalSettings.CurrentValue; globalSettings.OnChange(settings => _globalSettings = settings); _installEndpoint = new Lazy(GetInstallEndpoint); @@ -112,11 +120,22 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy ControllerActionDescriptor? controllerDescriptor = routeEndpoint.Metadata.GetMetadata(); TypeInfo? controllerTypeInfo = controllerDescriptor?.ControllerTypeInfo; if (controllerTypeInfo is not null && - (controllerTypeInfo.IsType() || controllerTypeInfo.IsType())) + (controllerTypeInfo.IsType() + || controllerTypeInfo.IsType())) { return; } + // If it's an UmbracoPageController we need to do some domain routing. + // We need to do this in oder to handle cultures for our Dictionary. + // This is because UmbracoPublishedContentCultureProvider is ued to set the Thread.CurrentThread.CurrentUICulture + // The CultureProvider is run before the actual routing, this means that our UmbracoVirtualPageFilterAttribute is hit AFTER the culture is set. + // Meaning we have to route the domain part already now, this is not pretty, but it beats having to look for content we know doesn't exist. + if (controllerTypeInfo is not null && controllerTypeInfo.IsType()) + { + await RouteVirtualRequestAsync(httpContext); + } + if (routeEndpoint.Order < lowestOrder) { // We have to ensure that the route is valid for the current request method. @@ -153,6 +172,22 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } } + private async Task RouteVirtualRequestAsync(HttpContext context) + { + if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) is false) + { + return; + } + + IPublishedRequestBuilder requestBuilder = + await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + _publishedRouter.RouteDomain(requestBuilder); + // This is just a temporary RouteValues object just for culture which will be overwritten later + // so we can just use a dummy action descriptor. + var umbracoRouteValues = new UmbracoRouteValues(requestBuilder.Build(), new ControllerActionDescriptor()); + context.Features.Set(umbracoRouteValues); + } + /// /// Replaces the first endpoint candidate with the specified endpoint, invalidating all other candidates, /// guaranteeing that the specified endpoint will be hit. From b2b112eb8b7964968650412313fe94e096dd3773 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:49:16 +0200 Subject: [PATCH 51/61] V13 QA Updated depedencies (#16606) * Bumped version of helper and builder * Removed faker --- .../package-lock.json | 28 +++++++------------ .../Umbraco.Tests.AcceptanceTest/package.json | 5 ++-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 82892582ce..e3bb3947ff 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,11 +7,10 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", - "@umbraco/playwright-testhelpers": "^1.0.25", + "@umbraco/json-models-builders": "^1.0.8", + "@umbraco/playwright-testhelpers": "^1.0.28", "camelize": "^1.0.0", "dotenv": "^16.0.2", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1" @@ -122,22 +121,20 @@ "dev": true }, "node_modules/@umbraco/json-models-builders": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.6.tgz", - "integrity": "sha512-bXwfXcpuqG1Ye714L9KJEGXuSzJfckysE/6CuPjdG8FqHWTE1brv28teR2oMw+ih8ca2u2zUboRgdzLEU/1D3Q==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.8.tgz", + "integrity": "sha512-qBiOwaFO0V/hRpByBnwWe65nlNvaKNQPQj17MCiUTkCf1LxkB1T0ZoQv50vvnLFx6xUBZKfLhEXqYmoJqsQsPg==", "dependencies": { - "camelize": "^1.0.0", - "faker": "^4.1.0" + "camelize": "^1.0.0" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.25.tgz", - "integrity": "sha512-6H452J6LhP0EHjF4jR7V7i0U8WPTiAbSyhN1J459BbbYEJ4QX1A2ZlCdA6VSBAsK1xYdMXD+yxsVJq7AAwiy9A==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.28.tgz", + "integrity": "sha512-AOk0eKkGV1Tyhb+iac9hyKzm2wCqwf+ELRQsiCWNVW8DxrPZiXIqQyjeI22YFuzkVJ3MJBDcoSwp7d31b1gm/w==", "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", + "@umbraco/json-models-builders": "^1.0.8", "camelize": "^1.0.0", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1" @@ -323,11 +320,6 @@ "node": "> 0.1.90" } }, - "node_modules/faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" - }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index adcdf64f1d..6dc84f51dc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -19,10 +19,9 @@ "wait-on": "^7.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^1.0.6", - "@umbraco/playwright-testhelpers": "^1.0.25", + "@umbraco/json-models-builders": "^1.0.8", + "@umbraco/playwright-testhelpers": "^1.0.28", "camelize": "^1.0.0", - "faker": "^4.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "xhr2": "^0.2.1", From 6e3a6917c8264b51caf7d3bf56907caeec0f223e Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:49:04 +0200 Subject: [PATCH 52/61] Decreased to 1 retry for this file only (#16623) --- .../tests/DefaultConfig/Tour/tours.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts index f5974db43d..03bbe86c78 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tour/tours.spec.ts @@ -3,6 +3,7 @@ import {test} from '@umbraco/playwright-testhelpers'; test.describe('Tours', () => { const timeout = 60000; + test.describe.configure({ retries: 1 }); test.beforeEach(async ({ page, umbracoApi }, testInfo) => { await umbracoApi.report.report(testInfo); await umbracoApi.login(); @@ -100,4 +101,4 @@ test.describe('Tours', () => { await expect(await umbracoUi.getGlobalHelp()).toBeVisible(); await getPercentage(17, timeout, page); }); -}); \ No newline at end of file +}); From e3d65967aafddf201b3cc7cf5adb195ed2be895e Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:45:13 +0200 Subject: [PATCH 53/61] Bump version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b9fa941e03..288af47e08 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.4.0", + "version": "13.5.0-rc", "assemblyVersion": { "precision": "build" }, From 2a57af8240f2b3f7bf458567e44892f5c05d98d2 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:52:49 +0200 Subject: [PATCH 54/61] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 6366776159..81d3527fe2 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 63667761595d0eba06137d358db214d7d72f7d14 +Subproject commit 81d3527fe2f0e1b5b889d6cfcbe7a152e7ff81dc From 9ad67e27c5f2b9ebf181f02dc90df1b18e85541a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 25 Jun 2024 10:03:19 +0200 Subject: [PATCH 55/61] Fixed test by moving when the publishing notification is fired. --- src/Umbraco.Core/Services/ContentService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index fb98a8ff88..dd61585203 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1188,14 +1188,6 @@ public class ContentService : RepositoryService, IContentService var allLangs = _languageRepository.GetMany().ToList(); - // Change state to publishing - content.PublishedState = PublishedState.Publishing; - var publishingNotification = new ContentPublishingNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(publishingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - // this will create the correct culture impact even if culture is * or null IEnumerable impacts = cultures.Select(culture => _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content)); @@ -1207,6 +1199,14 @@ public class ContentService : RepositoryService, IContentService content.PublishCulture(impact); } + // Change state to publishing + content.PublishedState = PublishedState.Publishing; + var publishingNotification = new ContentPublishingNotification(content, evtMsgs); + if (scope.Notifications.PublishCancelable(publishingNotification)) + { + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, publishingNotification.State, userId); scope.Complete(); return result; From 76bb2b084740697ccc3d49e9f16fdceb6e8070bb Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 25 Jun 2024 10:34:16 +0200 Subject: [PATCH 56/61] Add endpoint for calculating effective user start nodes (#16609) * Add endpoint for calculating effective user start nodes * Fix OpenAPI --- .../User/CalculateStartNodesUserController.cs | 59 ++++++++++ .../Factories/IUserPresentationFactory.cs | 2 + .../Factories/UserPresentationFactory.cs | 17 +++ src/Umbraco.Cms.Api.Management/OpenApi.json | 105 ++++++++++++++++++ .../CalculatedUserStartNodesResponseModel.cs | 14 +++ 5 files changed, 197 insertions(+) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs new file mode 100644 index 0000000000..98f1526ce9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs @@ -0,0 +1,59 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.User; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.User; + +[ApiVersion("1.0")] +public class CalculatedStartNodesUserController : UserControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; + private readonly IUserPresentationFactory _userPresentationFactory; + + public CalculatedStartNodesUserController( + IAuthorizationService authorizationService, + IUserService userService, + IUserPresentationFactory userPresentationFactory) + { + _authorizationService = authorizationService; + _userService = userService; + _userPresentationFactory = userPresentationFactory; + } + + [HttpGet("{id:guid}/calculate-start-nodes")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(CalculatedUserStartNodesResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task CalculatedStartNodes(CancellationToken cancellationToken, Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.UserPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + IUser? user = await _userService.GetAsync(id); + + if (user is null) + { + return UserOperationStatusResult(UserOperationStatus.UserNotFound); + } + + CalculatedUserStartNodesResponseModel responseModel = await _userPresentationFactory.CreateCalculatedUserStartNodesResponseModelAsync(user); + return Ok(responseModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs index 9af4ace9e1..a1e8a79edf 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs @@ -25,4 +25,6 @@ public interface IUserPresentationFactory Task CreateCurrentUserConfigurationModelAsync(); UserItemResponseModel CreateItemResponseModel(IUser user); + + Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 641f5883ed..9164be6772 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -212,6 +212,23 @@ public class UserPresentationFactory : IUserPresentationFactory }); } + public async Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user) + { + var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media); + var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); + ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document); + + return await Task.FromResult(new CalculatedUserStartNodesResponseModel() + { + Id = user.Key, + MediaStartNodeIds = mediaStartNodeKeys, + HasMediaRootAccess = HasRootAccess(mediaStartNodeIds), + DocumentStartNodeIds = documentStartNodeKeys, + HasDocumentRootAccess = HasRootAccess(contentStartNodeIds), + }); + } + private ISet GetKeysFromIds(IEnumerable? ids, UmbracoObjectTypes type) { IEnumerable? models = ids? diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 0adc3c36b7..11dee452ca 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -30298,6 +30298,66 @@ ] } }, + "/umbraco/management/api/v1/user/{id}/calculate-start-nodes": { + "get": { + "tags": [ + "User" + ], + "operationId": "GetUserByIdCalculateStartNodes", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CalculatedUserStartNodesResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/user/{id}/change-password": { "post": { "tags": [ @@ -33447,6 +33507,51 @@ }, "additionalProperties": false }, + "CalculatedUserStartNodesResponseModel": { + "required": [ + "documentStartNodeIds", + "hasDocumentRootAccess", + "hasMediaRootAccess", + "id", + "mediaStartNodeIds" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "documentStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasDocumentRootAccess": { + "type": "boolean" + }, + "mediaStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasMediaRootAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "ChangePasswordCurrentUserRequestModel": { "required": [ "newPassword" diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs new file mode 100644 index 0000000000..8cc71e8482 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.User; + +public class CalculatedUserStartNodesResponseModel +{ + public required Guid Id { get; init; } + + public ISet DocumentStartNodeIds { get; set; } = new HashSet(); + + public bool HasDocumentRootAccess { get; set; } + + public ISet MediaStartNodeIds { get; set; } = new HashSet(); + + public bool HasMediaRootAccess { get; set; } +} From 3c34eaf739e169ff076ffea06c55b377ebd892f2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 25 Jun 2024 11:20:52 +0200 Subject: [PATCH 57/61] Updated nuget packages --- Directory.Packages.props | 16 ++++++++-------- .../Umbraco.PublishedCache.NuCache.csproj | 1 - tests/Directory.Packages.props | 2 +- .../Umbraco.JsonSchema/Umbraco.JsonSchema.csproj | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c48c260baa..78d394ab7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,16 +5,16 @@ - + - + - + @@ -58,9 +58,9 @@ - - - + + + @@ -69,7 +69,7 @@ - + @@ -88,4 +88,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index 92f222525c..4894759e5f 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -10,7 +10,6 @@ - diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 4fce9e86f3..ba3af0bd42 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -5,7 +5,7 @@ - + diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index 7771d9c195..431674852e 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -7,7 +7,7 @@ - + From 0afb4f7283620a2afc5512d4aef9557e101c908b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 25 Jun 2024 13:47:25 +0200 Subject: [PATCH 58/61] Make GetHeaderValue support HttpContext unavailable (#16654) * Make GetHeaderValue tolerant for when the http context is not available. Now it just returns null. * Add unit tests --- .../Services/RequestHeaderHandler.cs | 8 +--- .../Services/RequestHeaderHandlerTests.cs | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs index 08d7a916e6..37651a4158 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs @@ -8,11 +8,5 @@ internal abstract class RequestHeaderHandler protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - protected string? GetHeaderValue(string headerName) - { - HttpContext httpContext = _httpContextAccessor.HttpContext ?? - throw new InvalidOperationException("Could not obtain an HTTP context"); - - return httpContext.Request.Headers[headerName]; - } + protected string? GetHeaderValue(string headerName) => _httpContextAccessor.HttpContext?.Request.Headers[headerName]; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs new file mode 100644 index 0000000000..9b4da511eb --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Delivery.Services; + +[TestFixture] +public class RequestHeaderHandlerTests +{ + private const string HeaderName = "TestHeader"; + [Test] + public void GetHeaderValue_return_null_when_http_context_is_unavailable() + { + IHttpContextAccessor httpContextAccessor = Mock.Of(); + + var sut = new TestRequestHeaderHandler(httpContextAccessor); + + Assert.IsNull(sut.TestGetHeaderValue(HeaderName)); + } + + [Test] + public void GetHeaderValue_return_header_value_when_http_context_is_available() + { + + const string headerValue = "TestValue"; + + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.Headers[HeaderName] = headerValue; + + IHttpContextAccessor httpContextAccessor = Mock.Of(); + Mock.Get(httpContextAccessor).Setup(x => x.HttpContext).Returns(httpContext); + + var sut = new TestRequestHeaderHandler(httpContextAccessor); + + Assert.AreEqual(headerValue, sut.TestGetHeaderValue(HeaderName)); + } +} + + +internal class TestRequestHeaderHandler : RequestHeaderHandler +{ + public TestRequestHeaderHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + + public string? TestGetHeaderValue(string headerName) => base.GetHeaderValue(headerName); +} From 0b868638e00f3cb28e2cbab3a49b7e1cf73cf287 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:24:47 +0200 Subject: [PATCH 59/61] V14 QA updated dictionary naming in E2E test (#16657) * Updated naming * Bumped version of helper --- .../package-lock.json | 8 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../Dictionary.spec.ts} | 110 +++++++++--------- 3 files changed, 60 insertions(+), 60 deletions(-) rename tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/{Translation/Translation.spec.ts => Dictionary/Dictionary.spec.ts} (55%) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 8383cb5028..25ef832219 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.7", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.61", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.63", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", @@ -146,9 +146,9 @@ "integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==" }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.61", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.61.tgz", - "integrity": "sha512-Y2RqGrjfLDCZGDPyix4r8LoSl/YaluzY8RHLlkdcbL5GojDprzfB0jN9P3ZbrCDvnu9hydA8qE6ElPq/Zw5qXw==", + "version": "2.0.0-beta.63", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.63.tgz", + "integrity": "sha512-fLXUcWNJupfGKkD6zOGg6WcU5cmqQ6gQkyIyG+UsKSrkgCxK23+N5LrOz2OVp2NZ8GQ8kB5pJ4izvCp+yMMOnA==", "dependencies": { "@umbraco/json-models-builders": "2.0.7", "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 11606d1a8e..8126ebf602 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.7", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.61", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.63", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts similarity index 55% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts index 355442a365..68ad9ee39b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -15,64 +15,64 @@ test.afterEach(async ({umbracoApi}) => { test('can create a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickCreateLink(); - await umbracoUi.translation.enterDictionaryName(dictionaryName); - await umbracoUi.translation.clickSaveButton(); + await umbracoUi.dictionary.clickCreateLink(); + await umbracoUi.dictionary.enterDictionaryName(dictionaryName); + await umbracoUi.dictionary.clickSaveButton(); // Assert expect(await umbracoApi.dictionary.doesNameExist(dictionaryName)).toBeTruthy(); - await umbracoUi.translation.isSuccessNotificationVisible(); - await umbracoUi.translation.clickLeftArrowButton(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); + await umbracoUi.dictionary.clickLeftArrowButton(); // Verify the dictionary item displays in the tree and in the list - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName); - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); }); test('can delete a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.deleteDictionary(); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.deleteDictionary(); // Assert - await umbracoUi.translation.isSuccessNotificationVisible(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); expect(await umbracoApi.dictionary.doesNameExist(dictionaryName)).toBeFalsy(); // Verify the dictionary item does not display in the tree - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName, false); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName, false); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the dictionary item does not display in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeFalsy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeFalsy(); }); test('can create a dictionary item in a dictionary', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); let parentDictionaryId = await umbracoApi.dictionary.create(parentDictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.translation.clickCreateDictionaryItemButton(); - await umbracoUi.translation.enterDictionaryName(dictionaryName); - await umbracoUi.translation.clickSaveButton(); + await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); + await umbracoUi.dictionary.clickCreateDictionaryItemButton(); + await umbracoUi.dictionary.enterDictionaryName(dictionaryName); + await umbracoUi.dictionary.clickSaveButton(); // Assert - await umbracoUi.translation.isSuccessNotificationVisible(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); const dictionaryChildren = await umbracoApi.dictionary.getChildren(parentDictionaryId); expect(dictionaryChildren[0].name).toEqual(dictionaryName); - await umbracoUi.translation.clickLeftArrowButton(); + await umbracoUi.dictionary.clickLeftArrowButton(); // Verify the new dictionary item displays in the list - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); // Verify the new dictionary item displays in the tree - await umbracoUi.translation.reloadTree(parentDictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName); + await umbracoUi.dictionary.reloadTree(parentDictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName); // Clean await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); @@ -82,12 +82,12 @@ test('can export a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); const dictionaryId = await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickExportMenu(); - const exportData = await umbracoUi.translation.exportDictionary(false); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickExportMenu(); + const exportData = await umbracoUi.dictionary.exportDictionary(false); // Assert expect(exportData).toEqual(dictionaryId + '.udt'); @@ -98,12 +98,12 @@ test('can export a dictionary item with descendants', {tag: '@smoke'}, async ({u await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); let parentDictionaryId = await umbracoApi.dictionary.create(parentDictionaryName); await umbracoApi.dictionary.create(dictionaryName, [], parentDictionaryId); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.translation.clickExportMenu(); - const exportData = await umbracoUi.translation.exportDictionary(true); + await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); + await umbracoUi.dictionary.clickExportMenu(); + const exportData = await umbracoUi.dictionary.exportDictionary(true); // Assert expect(exportData).toEqual(parentDictionaryId + '.udt'); @@ -119,20 +119,20 @@ test('can import a dictionary item', async ({umbracoApi, umbracoUi}) => { const importDictionaryName = 'TestImportDictionary'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickImportMenu(); - await umbracoUi.translation.importDictionary(udtFilePath); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickImportMenu(); + await umbracoUi.dictionary.importDictionary(udtFilePath); // Assert // Verify the imported dictionary item displays in the tree - await umbracoUi.translation.reloadTree(dictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importDictionaryName); + await umbracoUi.dictionary.reloadTree(dictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importDictionaryName); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the imported dictionary item displays in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importDictionaryName)).toBeTruthy(); }); test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { @@ -143,23 +143,23 @@ test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({u const importChildDictionaryName = 'TestImportChild'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickImportMenu(); - await umbracoUi.translation.importDictionary(udtFilePath); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickImportMenu(); + await umbracoUi.dictionary.importDictionary(udtFilePath); // Assert // Verify the imported dictionary items display in the tree - await umbracoUi.translation.reloadTree(dictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importParentDictionaryName); - await umbracoUi.translation.reloadTree(importParentDictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importChildDictionaryName); + await umbracoUi.dictionary.reloadTree(dictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importParentDictionaryName); + await umbracoUi.dictionary.reloadTree(importParentDictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importChildDictionaryName); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the imported dictionary items display in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importParentDictionaryName)).toBeTruthy(); - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importChildDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importParentDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importChildDictionaryName)).toBeTruthy(); }); // Skip this test as the search function is removed @@ -167,13 +167,13 @@ test.skip('can search a dictionary item in list when have results', async ({umbr // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.enterSearchKeywordAndPressEnter(dictionaryName); + await umbracoUi.dictionary.enterSearchKeywordAndPressEnter(dictionaryName); // Assert - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); }); // Skip this test as the search function is removed @@ -182,11 +182,11 @@ test.skip('can search a dictionary item in list when have no results', async ({u const emptySearchResultMessage = 'No Dictionary items to choose from'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.enterSearchKeywordAndPressEnter('xyz'); + await umbracoUi.dictionary.enterSearchKeywordAndPressEnter('xyz'); // Assert - await umbracoUi.translation.isSearchResultMessageDisplayEmpty(emptySearchResultMessage); + await umbracoUi.dictionary.isSearchResultMessageDisplayEmpty(emptySearchResultMessage); }); From 8ca637d0cb47b0d648f427931a6b834927a7c9da Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:18:05 +0200 Subject: [PATCH 60/61] V14: Fix source code editor not showing on fresh install / upgrade (#16655) * Create richtext with sourcecode by default * Add rich text migration * Create migration for migration "ace" value to "sourcecode" --- .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../V_14_0_0/MigrateRichTextConfiguration.cs | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 009811bc7d..53f7495822 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1934,7 +1934,7 @@ internal class DatabaseDataCreator EditorUiAlias = "Umb.PropertyEditorUi.TinyMCE", DbType = "Ntext", Configuration = - "{\"toolbar\":[\"ace\",\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"stylesheets\":[],\"maxImageSize\":500,\"mode\":\"classic\"}", + "{\"toolbar\":[\"sourcecode\",\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"stylesheets\":[],\"maxImageSize\":500,\"mode\":\"classic\"}", }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index a53833d99e..91ca3b4d58 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -84,5 +84,6 @@ public class UmbracoPlan : MigrationPlan // we need to re-run this migration, as it was flawed for V14 RC3 (the migration can run twice without any issues) To("{6FB5CA9E-C823-473B-A14C-FE760D75943C}"); To("{827360CA-0855-42A5-8F86-A51F168CB559}"); + To("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs new file mode 100644 index 0000000000..950259f575 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs @@ -0,0 +1,32 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; + +public class MigrateRichTextConfiguration : MigrationBase +{ + + public MigrateRichTextConfiguration(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.EditorAlias.Equals(Constants.PropertyEditors.Aliases.RichText)); + + List dataTypeDtos = Database.Fetch(sql); + + foreach (DataTypeDto dataTypeDto in dataTypeDtos) + { + // Update the configuration + dataTypeDto.Configuration = dataTypeDto.Configuration?.Replace("\"ace", "\"sourcecode"); + Database.Update(dataTypeDto); + } + } +} From 7e8dd02e793aa61fc31fafe41b1a908bcd55f099 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:32:45 +0200 Subject: [PATCH 61/61] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index c0a342b5e8..9077e80b32 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit c0a342b5e88535c8f9c8bf36dd15fa493f3822a6 +Subproject commit 9077e80b3298c3ef9ca491fca7a33cc662ea6f5b