diff --git a/.github/workflows/add-issues-to-review-project.yml b/.github/workflows/add-issues-to-review-project.yml
deleted file mode 100644
index 0d89451373..0000000000
--- a/.github/workflows/add-issues-to-review-project.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: Add issues to review project
-
-on:
- issues:
- types:
- - opened
-
-permissions:
- contents: read
-
-jobs:
- get-user-type:
- runs-on: ubuntu-latest
- outputs:
- ignored: ${{ steps.set-output.outputs.ignored }}
- steps:
- - name: Install dependencies
- run: |
- npm install node-fetch@2
- - uses: actions/github-script@v5
- name: "Determing HQ user or not"
- id: set-output
- with:
- script: |
- const fetch = require('node-fetch');
- const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/users/IsIgnoredUser', {
- method: 'post',
- body: JSON.stringify('${{ github.event.issue.user.login }}'),
- headers: {
- 'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
- 'Content-Type': 'application/json'
- }
- });
-
- var isIgnoredUser = true;
- try {
- if(response.status === 200) {
- const data = await response.text();
- isIgnoredUser = data === "true";
- } else {
- console.log("Returned data not indicate success:", response.status);
- }
- } catch(error) {
- console.log(error);
- };
- core.setOutput("ignored", isIgnoredUser);
- console.log("Ignored is", isIgnoredUser);
- add-to-project:
- permissions:
- repository-projects: write # for actions/add-to-project
- if: needs.get-user-type.outputs.ignored == 'false'
- runs-on: ubuntu-latest
- needs: [get-user-type]
- steps:
- - uses: actions/add-to-project@main
- with:
- project-url: https://github.com/orgs/${{ github.repository_owner }}/projects/21
- github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
diff --git a/Directory.Build.props b/Directory.Build.props
index 6ca48f7712..a87ecd9ae7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -29,9 +29,9 @@
- true
- false
- 12.0.0-rc1
+ false
+ true
+ 12.0.0truetrue
diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
index 4048af3fb4..e2d7c8e38d 100644
--- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
+++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
@@ -2,16 +2,18 @@
Umbraco CMS - API CommonContains the bits and pieces that are shared between the Umbraco CMS APIs.
- true
- false
- Umbraco.Cms.Api.Common
- Umbraco.Cms.Api.Common
+
+
+
+
+
+
diff --git a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
index 33a3105b73..a37f74f541 100644
--- a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
+++ b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
@@ -2,14 +2,8 @@
Umbraco CMS - Delivery APIContains the presentation layer for the Umbraco CMS Delivery API.
- true
- false
- Umbraco.Cms.Api.Delivery
- Umbraco.Cms.Api.Delivery
- Umbraco.Cms.Api.Delivery
-
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 dc0299defd..14c203bad6 100644
--- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
+++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
@@ -2,8 +2,6 @@
Umbraco CMS - Imaging - ImageSharp 2Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.
-
- false
diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj
index cc1571540e..6e8268dc2a 100644
--- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj
+++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj
@@ -1,6 +1,7 @@
-
+
- Umbraco CMS - EF Core - SqlServer migrations
+ Umbraco CMS - Persistence - Entity Framework Core - SQL Server migrations
+ Adds support for Entity Framework Core SQL Server migrations to Umbraco CMS.false
diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
index d3274a5005..e81e7605ca 100644
--- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
+++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
@@ -1,6 +1,7 @@
- Umbraco CMS - EF Core - Sqlite migrations
+ Umbraco CMS - Persistence - Entity Framework Core - SQLite migrations
+ Adds support for Entity Framework Core SQLite migrations to Umbraco CMS.false
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
index 8d7a835ab1..23cc8c74d1 100644
--- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
+++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
@@ -1,8 +1,7 @@
- Umbraco CMS - Persistence - EFCore
-
- false
+ Umbraco CMS - Persistence - Entity Framework Core
+ Adds support for Entity Framework Core to Umbraco CMS.
@@ -22,5 +21,4 @@
<_Parameter1>Umbraco.Tests.Integration
-
diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
index 055da32d75..511826114d 100644
--- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
+++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
@@ -81,15 +81,16 @@ public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase.TryParse(setting.Substring("SqlServer.".Length), out VersionName versionName, true))
+ if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") || !Enum.TryParse(setting.AsSpan("SqlServer.".Length), true, out VersionName versionName))
{
versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName;
}
+
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected");
}
+
return DatabaseType.SqlServer2012;
}
diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
index e7dcdd5bec..37653bc9a8 100644
--- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
+++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
@@ -4,8 +4,6 @@
Installs Umbraco CMS with minimal dependencies in your ASP.NET Core project.falsefalse
-
- false
diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml
deleted file mode 100644
index f486c5dc88..0000000000
--- a/src/Umbraco.Core/CompatibilitySuppressions.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
- CP0001
- T:Umbraco.New.Cms.Core.Models.PagedModel`1
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0005
- M:Umbraco.Cms.Core.Models.PublishedContent.PublishedPropertyBase.GetDeliveryApiValue(System.Boolean,System.String,System.String)
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Events.IEventAggregator.Publish``2(System.Collections.Generic.IEnumerable{``0})
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Events.IEventAggregator.PublishAsync``2(System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken)
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedProperty.GetDeliveryApiValue(System.Boolean,System.String,System.String)
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.ConvertInterToDeliveryApiObject(Umbraco.Cms.Core.Models.PublishedContent.IPublishedElement,Umbraco.Cms.Core.PropertyEditors.PropertyCacheLevel,System.Object,System.Boolean,System.Boolean)
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.ConvertInterToDeliveryApiObject(Umbraco.Cms.Core.Models.PublishedContent.IPublishedElement,Umbraco.Cms.Core.PropertyEditors.PropertyCacheLevel,System.Object,System.Boolean)
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- P:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.DeliveryApiCacheLevel
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
- CP0006
- P:Umbraco.Cms.Core.Scoping.ICoreScope.Locks
- lib/net7.0/Umbraco.Core.dll
- lib/net7.0/Umbraco.Core.dll
- true
-
-
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index 290836d31e..e209e45cc4 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -191,7 +191,7 @@ public class ContentSettings
/// Gets or sets a value for the macro error behaviour.
///
[DefaultValue(StaticMacroErrors)]
- public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors);
+ public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors);
///
/// Gets or sets a value for the collection of file extensions that are disallowed for upload.
@@ -243,7 +243,7 @@ public class ContentSettings
public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced;
///
- /// Get or sets the model representing the global content version cleanup policy
+ /// Gets or sets the model representing the global content version cleanup policy
///
public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new();
diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
index c973f59025..00b3f56583 100644
--- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
@@ -25,8 +25,7 @@ public class HealthChecksNotificationMethodSettings
/// Gets or sets a value for the health check notifications reporting verbosity.
///
[DefaultValue(StaticVerbosity)]
- public HealthCheckNotificationVerbosity Verbosity { get; set; } =
- Enum.Parse(StaticVerbosity);
+ public HealthCheckNotificationVerbosity Verbosity { get; set; } = Enum.Parse(StaticVerbosity);
///
/// Gets or sets a value indicating whether the health check notifications should occur on failures only.
diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
index 2329c73d66..c8df39b49a 100644
--- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
@@ -23,8 +23,7 @@ public class HostingSettings
/// Gets or sets a value for the location of temporary files.
///
[DefaultValue(StaticLocalTempStorageLocation)]
- public LocalTempStorage LocalTempStorageLocation { get; set; } =
- Enum.Parse(StaticLocalTempStorageLocation);
+ public LocalTempStorage LocalTempStorageLocation { get; set; } = Enum.Parse(StaticLocalTempStorageLocation);
///
/// Gets or sets a value indicating whether umbraco is running in [debug mode].
diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
index 0e7e1812c6..be86cf1f2b 100644
--- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
@@ -22,7 +22,7 @@ public class ModelsBuilderSettings
/// Gets or sets a value for the models mode.
///
[DefaultValue(StaticModelsMode)]
- public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode);
+ public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode);
///
/// Gets or sets a value for models namespace.
@@ -52,10 +52,9 @@ public class ModelsBuilderSettings
return _flagOutOfDateModels;
}
- set => _flagOutOfDateModels = value;
+ set => _flagOutOfDateModels = value;
}
-
///
/// Gets or sets a value for the models directory.
///
diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
index b88dbb5d0d..490e03096d 100644
--- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
@@ -24,8 +24,7 @@ public class NuCacheSettings
/// The serializer type that nucache uses to persist documents in the database.
///
[DefaultValue(StaticNuCacheSerializerType)]
- public NuCacheSerializerType NuCacheSerializerType { get; set; } =
- Enum.Parse(StaticNuCacheSerializerType);
+ public NuCacheSerializerType NuCacheSerializerType { get; set; } = Enum.Parse(StaticNuCacheSerializerType);
///
/// The paging size to use for nucache SQL queries.
diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs
index 09c55c784b..6ec84ffe1e 100644
--- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs
@@ -19,8 +19,7 @@ public class RuntimeMinificationSettings
/// The cache buster type to use
///
[DefaultValue(StaticCacheBuster)]
- public RuntimeMinificationCacheBuster CacheBuster { get; set; } =
- Enum.Parse(StaticCacheBuster);
+ public RuntimeMinificationCacheBuster CacheBuster { get; set; } = Enum.Parse(StaticCacheBuster);
///
/// The unique version string used if CacheBuster is 'Version'.
diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
index 7d5c126542..92229b1b6d 100644
--- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
@@ -74,8 +74,7 @@ public class SmtpSettings : ValidatableEntryBase
/// Gets or sets a value for the secure socket options.
///
[DefaultValue(StaticSecureSocketOptions)]
- public SecureSocketOptions SecureSocketOptions { get; set; } =
- Enum.Parse(StaticSecureSocketOptions);
+ public SecureSocketOptions SecureSocketOptions { get; set; } = Enum.Parse(StaticSecureSocketOptions);
///
/// Gets or sets a value for the SMTP pick-up directory.
@@ -86,7 +85,7 @@ public class SmtpSettings : ValidatableEntryBase
/// Gets or sets a value for the SMTP delivery method.
///
[DefaultValue(StaticDeliveryMethod)]
- public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod);
+ public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod);
///
/// Gets or sets a value for the SMTP user name.
diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
index c4dff7a542..12f71c7b44 100644
--- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
@@ -75,7 +75,7 @@ public class WebRoutingSettings
/// Gets or sets a value for the URL provider mode ().
///
[DefaultValue(StaticUrlProviderMode)]
- public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode);
+ public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode);
///
/// Gets or sets a value for the Umbraco application URL.
diff --git a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs
index 6cb2642b15..242cfb8e35 100644
--- a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs
+++ b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs
@@ -1,6 +1,10 @@
+using System.Globalization;
+
namespace Umbraco.Cms.Core.Dictionary;
public interface ICultureDictionaryFactory
{
ICultureDictionary CreateDictionary();
+
+ ICultureDictionary CreateDictionary(CultureInfo specificCulture) => throw new NotImplementedException();
}
diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs
index de968f1676..36e1acbce8 100644
--- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs
+++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
@@ -47,7 +48,7 @@ internal class DefaultCultureDictionary : ICultureDictionary
}
///
- /// Returns the current culture
+ /// Returns the defualt umbraco's back office culture
///
public CultureInfo Culture => _specificCulture ?? Thread.CurrentThread.CurrentUICulture;
diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs
index 4c4eb030cc..2f00114c13 100644
--- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs
+++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Services;
@@ -23,4 +24,7 @@ public class DefaultCultureDictionaryFactory : ICultureDictionaryFactory
public ICultureDictionary CreateDictionary() =>
new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache);
+
+ public ICultureDictionary CreateDictionary(CultureInfo specificCulture) =>
+ new DefaultCultureDictionary(specificCulture, _localizationService, _appCaches.RequestCache);
}
diff --git a/src/Umbraco.Core/Enum.cs b/src/Umbraco.Core/Enum.cs
index 6084dfe971..03dd0d51bc 100644
--- a/src/Umbraco.Core/Enum.cs
+++ b/src/Umbraco.Core/Enum.cs
@@ -22,7 +22,7 @@ public static class Enum
IntToValue = new Dictionary();
ValueToName = new Dictionary();
SensitiveNameToValue = new Dictionary();
- InsensitiveNameToValue = new Dictionary();
+ InsensitiveNameToValue = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (T value in Values)
{
@@ -31,15 +31,15 @@ public static class Enum
IntToValue[Convert.ToInt32(value)] = value;
ValueToName[value] = name!;
SensitiveNameToValue[name!] = value;
- InsensitiveNameToValue[name!.ToLowerInvariant()] = value;
+ InsensitiveNameToValue[name!] = value;
}
}
- public static bool IsDefined(T value) => ValueToName.Keys.Contains(value);
+ public static bool IsDefined(T value) => ValueToName.ContainsKey(value);
- public static bool IsDefined(string value) => SensitiveNameToValue.Keys.Contains(value);
+ public static bool IsDefined(string value) => SensitiveNameToValue.ContainsKey(value);
- public static bool IsDefined(int value) => IntToValue.Keys.Contains(value);
+ public static bool IsDefined(int value) => IntToValue.ContainsKey(value);
public static IEnumerable GetValues() => Values;
@@ -50,28 +50,15 @@ public static class Enum
public static T Parse(string value, bool ignoreCase = false)
{
Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue;
- if (ignoreCase)
- {
- value = value.ToLowerInvariant();
- }
- if (names.TryGetValue(value, out T parsed))
- {
- return parsed;
- }
+ return names.TryGetValue(value, out T parsed) ? parsed : Throw();
- throw new ArgumentException(
- $"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.",
- nameof(value));
+ T Throw() => throw new ArgumentException($"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", nameof(value));
}
public static bool TryParse(string value, out T returnValue, bool ignoreCase = false)
{
Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue;
- if (ignoreCase)
- {
- value = value.ToLowerInvariant();
- }
return names.TryGetValue(value, out returnValue);
}
@@ -83,7 +70,7 @@ public static class Enum
return null;
}
- if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out T parsed))
+ if (InsensitiveNameToValue.TryGetValue(value, out T parsed))
{
return parsed;
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/Language.cs b/src/Umbraco.Core/Models/ContentEditing/Language.cs
index 112aeb5aac..99c011d608 100644
--- a/src/Umbraco.Core/Models/ContentEditing/Language.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/Language.cs
@@ -22,7 +22,7 @@ public class Language
[DataMember(Name = "isMandatory")]
public bool IsMandatory { get; set; }
- [Obsolete("This will be replaced by fallback language ISO code in V13.")]
+ [Obsolete("This will be replaced by fallback language ISO code in V14.")]
[DataMember(Name = "fallbackLanguageId")]
public int? FallbackLanguageId { get; set; }
}
diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs
index 90576a85e3..b0e787de02 100644
--- a/src/Umbraco.Core/Models/DictionaryItem.cs
+++ b/src/Umbraco.Core/Models/DictionaryItem.cs
@@ -34,7 +34,7 @@ public class DictionaryItem : EntityBase, IDictionaryItem
_translations = new List();
}
- [Obsolete("This will be removed in V13.")]
+ [Obsolete("This will be removed in V14.")]
public Func? GetLanguage { get; set; }
///
diff --git a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
index 09654d5137..341f185ff9 100644
--- a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
+++ b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
@@ -10,7 +10,7 @@ public static class DictionaryItemExtensions
///
///
///
- [Obsolete("This will be replaced in V13 by a corresponding method accepting language ISO code instead of language ID.")]
+ [Obsolete("This will be replaced in V14 by a corresponding method accepting language ISO code instead of language ID.")]
public static string? GetTranslatedValue(this IDictionaryItem d, int languageId)
{
IDictionaryTranslation? trans = d.Translations.FirstOrDefault(x => x.LanguageId == languageId);
@@ -22,7 +22,7 @@ public static class DictionaryItemExtensions
///
///
///
- [Obsolete("Warning: This method ONLY works in very specific scenarios. It will be removed in V13.")]
+ [Obsolete("Warning: This method ONLY works in very specific scenarios. It will be removed in V14.")]
public static string? GetDefaultValue(this IDictionaryItem d)
{
IDictionaryTranslation? defaultTranslation = d.Translations.FirstOrDefault(x => x.Language?.Id == 1);
diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs
index ab79b77e44..7f4471785c 100644
--- a/src/Umbraco.Core/Models/DictionaryTranslation.cs
+++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs
@@ -1,5 +1,8 @@
using System.Runtime.Serialization;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Entities;
+using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.Models;
@@ -11,6 +14,7 @@ namespace Umbraco.Cms.Core.Models;
public class DictionaryTranslation : EntityBase, IDictionaryTranslation
{
private ILanguage? _language;
+ private string? _languageIsoCode;
// note: this will be memberwise cloned
private string _value;
@@ -20,6 +24,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation
_language = language ?? throw new ArgumentNullException("language");
LanguageId = _language.Id;
_value = value;
+ LanguageIsoCode = language.IsoCode;
}
public DictionaryTranslation(ILanguage language, string value, Guid uniqueId)
@@ -27,17 +32,18 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation
_language = language ?? throw new ArgumentNullException("language");
LanguageId = _language.Id;
_value = value;
+ LanguageIsoCode = language.IsoCode;
Key = uniqueId;
}
- [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V13.")]
+ [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V14.")]
public DictionaryTranslation(int languageId, string value)
{
LanguageId = languageId;
_value = value;
}
- [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V13.")]
+ [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V14.")]
public DictionaryTranslation(int languageId, string value, Guid uniqueId)
{
LanguageId = languageId;
@@ -58,7 +64,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation
/// returned
/// on a callback.
///
- [Obsolete("This will be removed in V13. From V13 onwards you should get languages by ISO code from ILanguageService.")]
+ [Obsolete("This will be removed in V14. From V14 onwards you should get languages by ISO code from ILanguageService.")]
[DataMember]
[DoNotClone]
public ILanguage? Language
@@ -86,7 +92,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation
}
}
- [Obsolete("This will be replaced by language ISO code in V13.")]
+ [Obsolete("This will be replaced by language ISO code in V14.")]
public int LanguageId { get; private set; }
///
@@ -99,6 +105,24 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation
set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value));
}
+ ///
+ public string LanguageIsoCode
+ {
+ get
+ {
+ // TODO: this won't be necessary after obsoleted ctors are removed in v14.
+ if (_languageIsoCode is null)
+ {
+ var _languageService = StaticServiceProvider.Instance.GetRequiredService();
+ _languageIsoCode = _languageService.GetLanguageById(LanguageId)?.IsoCode ?? string.Empty;
+ }
+
+ return _languageIsoCode;
+ }
+
+ private set => SetPropertyValueAndDetectChanges(value, ref _languageIsoCode!, nameof(LanguageIsoCode));
+ }
+
protected override void PerformDeepClone(object clone)
{
base.PerformDeepClone(clone);
diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs
index 45d71e3f9b..8f8d9ffaa4 100644
--- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs
+++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs
@@ -6,18 +6,24 @@ namespace Umbraco.Cms.Core.Models;
public interface IDictionaryTranslation : IEntity, IRememberBeingDirty
{
///
- /// Gets or sets the for the translation
+ /// Gets or sets the for the translation.
///
- [Obsolete("This will be removed in V13. From V13 onwards you should get languages by ISO code from ILanguageService.")]
+ [Obsolete("This will be removed in V14. From V14 onwards you should get languages by ISO code from ILanguageService.")]
[DataMember]
ILanguage? Language { get; set; }
- [Obsolete("This will be replaced by language ISO code in V13.")]
+ [Obsolete("This will be replaced by language ISO code in V14.")]
int LanguageId { get; }
///
- /// Gets or sets the translated text
+ /// Gets or sets the translated text.
///
[DataMember]
string Value { get; set; }
+
+ ///
+ /// Gets the ISO code of the language.
+ ///
+ [DataMember]
+ string LanguageIsoCode => Language?.IsoCode ?? string.Empty;
}
diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs
index 88c76ae7b0..885833cd5c 100644
--- a/src/Umbraco.Core/Models/ILanguage.cs
+++ b/src/Umbraco.Core/Models/ILanguage.cs
@@ -55,7 +55,24 @@ public interface ILanguage : IEntity, IRememberBeingDirty
/// define fallback strategies when a value does not exist for a requested language.
///
///
- [Obsolete("This will be replaced by fallback language ISO code in V13.")]
+ [Obsolete("This will be replaced by fallback language ISO code in V14.")]
[DataMember]
int? FallbackLanguageId { get; set; }
+
+
+ ///
+ /// Gets or sets the ISO code of a fallback language.
+ ///
+ ///
+ ///
+ /// The fallback language can be used in multi-lingual scenarios, to help
+ /// define fallback strategies when a value does not exist for a requested language.
+ ///
+ ///
+ [DataMember]
+ string? FallbackIsoCode
+ {
+ get => null;
+ set { }
+ }
}
diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs
index 9871cf3eed..62a65f086b 100644
--- a/src/Umbraco.Core/Models/Language.cs
+++ b/src/Umbraco.Core/Models/Language.cs
@@ -1,6 +1,5 @@
using System.Globalization;
using System.Runtime.Serialization;
-using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models;
@@ -14,6 +13,7 @@ public class Language : EntityBase, ILanguage
{
private string _cultureName;
private int? _fallbackLanguageId;
+ private string? _fallbackLanguageIsoCode;
private bool _isDefaultVariantLanguage;
private string _isoCode;
private bool _mandatory;
@@ -74,10 +74,17 @@ public class Language : EntityBase, ILanguage
}
///
- [Obsolete("This will be replaced by fallback language ISO code in V13.")]
+ [Obsolete("This will be replaced by fallback language ISO code in V14.")]
public int? FallbackLanguageId
{
get => _fallbackLanguageId;
set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId));
}
+
+ ///
+ public string? FallbackIsoCode
+ {
+ get => _fallbackLanguageIsoCode;
+ set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageIsoCode, nameof(FallbackIsoCode));
+ }
}
diff --git a/src/Umbraco.Core/Scoping/CoreScope.cs b/src/Umbraco.Core/Scoping/CoreScope.cs
index a05b44f4a7..7fe6c400fb 100644
--- a/src/Umbraco.Core/Scoping/CoreScope.cs
+++ b/src/Umbraco.Core/Scoping/CoreScope.cs
@@ -231,7 +231,7 @@ public class CoreScope : ICoreScope
}
}
- private void HandleScopedFileSystems()
+ protected void HandleScopedFileSystems()
{
if (_shouldScopeFileSystems == true)
{
@@ -250,7 +250,7 @@ public class CoreScope : ICoreScope
_parentScope = coreScope;
}
- private void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
+ protected void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
private void EnsureNotDisposed()
{
diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs
index 72d46b2fb6..4c0661594c 100644
--- a/src/Umbraco.Core/Services/NotificationService.cs
+++ b/src/Umbraco.Core/Services/NotificationService.cs
@@ -565,49 +565,58 @@ public class NotificationService : INotificationService
}
}
- private void Process(BlockingCollection notificationRequests) =>
- ThreadPool.QueueUserWorkItem(state =>
+ private void Process(BlockingCollection notificationRequests)
+ {
+ // We need to suppress the flow of the ExecutionContext when starting a new thread.
+ // Otherwise our scope stack will leak into the context of the new thread, leading to disposing race conditions.
+ using (ExecutionContext.SuppressFlow())
{
- if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ ThreadPool.QueueUserWorkItem(state =>
{
- _logger.LogDebug("Begin processing notifications.");
- }
- while (true)
- {
- // stay on for 8s
- while (notificationRequests.TryTake(out NotificationRequest? request, 8 * 1000))
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
- try
+ _logger.LogDebug("Begin processing notifications.");
+ }
+
+ while (true)
+ {
+ // stay on for 8s
+ while (notificationRequests.TryTake(out NotificationRequest? request, 8 * 1000))
{
- _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter()
- .GetResult();
- if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ try
{
- _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email);
+ _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter()
+ .GetResult();
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ {
+ _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred sending notification");
}
}
- catch (Exception ex)
+
+ lock (Locker)
{
- _logger.LogError(ex, "An error occurred sending notification");
+ if (notificationRequests.Count > 0)
+ {
+ continue; // last chance
+ }
+
+ _running = false; // going down
+ break;
}
}
- lock (Locker)
+ if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
- if (notificationRequests.Count > 0)
- {
- continue; // last chance
- }
-
- _running = false; // going down
- break;
+ _logger.LogDebug("Done processing notifications.");
}
- }
- if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
- {
- _logger.LogDebug("Done processing notifications.");
- }
- });
+ });
+ }
+ }
private class NotificationRequest
{
diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml
deleted file mode 100644
index a9076b112d..0000000000
--- a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
- CP0001
- T:Umbraco.Cms.Core.Cache.DistributedCacheBinder
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0001
- T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.ClearCsrfCookies
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0001
- T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.DeleteLogViewerQueryFile
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0001
- T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.RebuildPublishedSnapshot
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0001
- T:Umbraco.Extensions.DistributedCacheExtensions
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.Execute(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String)
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Infrastructure.Migrations.IMigrationContext.AddPostMigration``1
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlan.AddPostMigration``1
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlan.get_PostMigrationTypes
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlanExecutor.Execute(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String)
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0002
- M:Umbraco.Cms.Infrastructure.Migrations.Upgrade.Upgrader.Execute(Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor,Umbraco.Cms.Core.Scoping.IScopeProvider,Umbraco.Cms.Core.Services.IKeyValueService)
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.ExecutePlan(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String)
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0006
- M:Umbraco.Cms.Infrastructure.Search.IUmbracoIndexingHandler.RemoveProtectedContent
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
- CP0006
- P:Umbraco.Cms.Infrastructure.Examine.IUmbracoIndex.SupportProtectedContent
- lib/net7.0/Umbraco.Infrastructure.dll
- lib/net7.0/Umbraco.Infrastructure.dll
- true
-
-
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index 8c1e0e2a54..b847743076 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -86,7 +86,7 @@ public class DatabaseSchemaCreator
};
private readonly IUmbracoDatabase _database;
- private readonly IOptionsMonitor _defaultDataCreationSettings;
+ private readonly IOptionsMonitor _installDefaultDataSettings;
private readonly IEventAggregator _eventAggregator;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
@@ -105,7 +105,7 @@ public class DatabaseSchemaCreator
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
_eventAggregator = eventAggregator;
- _defaultDataCreationSettings = defaultDataCreationSettings;
+ _installDefaultDataSettings = defaultDataCreationSettings; // TODO (V13): Rename this parameter to installDefaultDataSettings.
if (_database?.SqlContext?.SqlSyntax == null)
{
@@ -165,7 +165,7 @@ public class DatabaseSchemaCreator
var dataCreation = new DatabaseDataCreator(
_database, _loggerFactory.CreateLogger(),
_umbracoVersion,
- _defaultDataCreationSettings);
+ _installDefaultDataSettings);
foreach (Type table in _orderedTables)
{
CreateTable(false, table, dataCreation);
@@ -442,7 +442,7 @@ public class DatabaseSchemaCreator
_database,
_loggerFactory.CreateLogger(),
_umbracoVersion,
- _defaultDataCreationSettings));
+ _installDefaultDataSettings));
}
///
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs
index 9ab958c306..6ef14238af 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories;
internal static class LanguageFactory
{
- public static ILanguage BuildEntity(LanguageDto dto)
+ public static ILanguage BuildEntity(LanguageDto dto, string? fallbackIsoCode)
{
ArgumentNullException.ThrowIfNull(dto);
if (dto.IsoCode is null)
@@ -22,6 +22,7 @@ internal static class LanguageFactory
IsDefault = dto.IsDefault,
IsMandatory = dto.IsMandatory,
FallbackLanguageId = dto.FallbackLanguageId,
+ FallbackIsoCode = fallbackIsoCode
};
// Reset dirty initial properties
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
index 398a55ebaf..590fae26c0 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
@@ -120,7 +120,19 @@ internal class LanguageRepository : EntityRepositoryBase, ILangu
new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false);
protected ILanguage ConvertFromDto(LanguageDto dto)
- => LanguageFactory.BuildEntity(dto);
+ {
+ // yes, we want to lock _codeIdMap
+ lock (_codeIdMap)
+ {
+ string? fallbackIsoCode = null;
+ if (dto.FallbackLanguageId.HasValue && _idCodeMap.TryGetValue(dto.FallbackLanguageId.Value, out fallbackIsoCode) == false)
+ {
+ throw new ArgumentException($"The ISO code map did not contain ISO code for fallback language ID: {dto.FallbackLanguageId}. Please reload the caches.");
+ }
+
+ return LanguageFactory.BuildEntity(dto, fallbackIsoCode);
+ }
+ }
// do NOT leak that language, it's not deep-cloned!
private ILanguage GetDefault()
@@ -172,20 +184,25 @@ internal class LanguageRepository : EntityRepositoryBase, ILangu
sql.OrderBy(x => x.Id);
// get languages
- var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList();
+ List? languageDtos = Database.Fetch(sql) ?? new List();
- // initialize the code-id map
- lock (_codeIdMap)
+ // initialize the code-id map if we've reloaded the entire set of languages
+ if (ids?.Any() == false)
{
- _codeIdMap.Clear();
- _idCodeMap.Clear();
- foreach (ILanguage language in languages)
+ lock (_codeIdMap)
{
- _codeIdMap[language.IsoCode] = language.Id;
- _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant();
+ _codeIdMap.Clear();
+ _idCodeMap.Clear();
+ foreach (LanguageDto languageDto in languageDtos)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(languageDto.IsoCode, nameof(LanguageDto.IsoCode));
+ _codeIdMap[languageDto.IsoCode] = languageDto.Id;
+ _idCodeMap[languageDto.Id] = languageDto.IsoCode;
+ }
}
}
+ var languages = languageDtos.Select(ConvertFromDto).OrderBy(x => x.Id).ToList();
return languages;
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
index 4fc868256b..f58051fc8a 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
@@ -226,7 +226,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
if (html is not null)
{
- var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId, _imageUrlGenerator);
+ var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages(
+ html, mediaParentId, userId);
+ var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId);
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
rte.Value = editorValueWithMediaUrlsRemoved;
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
index 5044a5b13e..8dbe6ad5b3 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
@@ -1,8 +1,14 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
using HtmlAgilityPack;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
@@ -13,6 +19,7 @@ using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -30,7 +37,11 @@ public sealed class RichTextEditorPastedImages
private readonly IShortStringHelper _shortStringHelper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly string _tempFolderAbsolutePath;
+ private readonly IImageUrlGenerator _imageUrlGenerator;
+ private readonly ContentSettings _contentSettings;
+ private readonly Dictionary _uploadedImages = new();
+ [Obsolete("Use the ctor which takes an IImageUrlGenerator and IOptions instead, scheduled for removal in v14")]
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
ILogger logger,
@@ -41,6 +52,33 @@ public sealed class RichTextEditorPastedImages
MediaUrlGeneratorCollection mediaUrlGenerators,
IShortStringHelper shortStringHelper,
IPublishedUrlProvider publishedUrlProvider)
+ : this(
+ umbracoContextAccessor,
+ logger,
+ hostingEnvironment,
+ mediaService,
+ contentTypeBaseServiceProvider,
+ mediaFileManager,
+ mediaUrlGenerators,
+ shortStringHelper,
+ publishedUrlProvider,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService>())
+ {
+ }
+
+ public RichTextEditorPastedImages(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ ILogger logger,
+ IHostingEnvironment hostingEnvironment,
+ IMediaService mediaService,
+ IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
+ MediaFileManager mediaFileManager,
+ MediaUrlGeneratorCollection mediaUrlGenerators,
+ IShortStringHelper shortStringHelper,
+ IPublishedUrlProvider publishedUrlProvider,
+ IImageUrlGenerator imageUrlGenerator,
+ IOptions contentSettings)
{
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
@@ -53,15 +91,132 @@ public sealed class RichTextEditorPastedImages
_mediaUrlGenerators = mediaUrlGenerators;
_shortStringHelper = shortStringHelper;
_publishedUrlProvider = publishedUrlProvider;
+ _imageUrlGenerator = imageUrlGenerator;
+ _contentSettings = contentSettings.Value;
_tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads);
-
}
///
- /// Used by the RTE (and grid RTE) for drag/drop/persisting images
+ /// Used by the RTE (and grid RTE) for converting inline base64 images to Media items.
///
- public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
+ /// HTML from the Rich Text Editor property editor.
+ ///
+ ///
+ /// Formatted HTML.
+ /// Thrown if image extension is not allowed
+ internal string FindAndPersistEmbeddedImages(string html, Guid mediaParentFolder, int userId)
+ {
+ // Find all img's that has data-tmpimg attribute
+ // Use HTML Agility Pack - https://html-agility-pack.net
+ var htmlDoc = new HtmlDocument();
+ htmlDoc.LoadHtml(html);
+
+ HtmlNodeCollection? imagesWithDataUris = htmlDoc.DocumentNode.SelectNodes("//img");
+ if (imagesWithDataUris is null || imagesWithDataUris.Count is 0)
+ {
+ return html;
+ }
+
+ foreach (HtmlNode? img in imagesWithDataUris)
+ {
+ var srcValue = img.GetAttributeValue("src", string.Empty);
+
+ // Ignore src-less images
+ if (string.IsNullOrEmpty(srcValue))
+ {
+ continue;
+ }
+
+ // Take only images that have a "data:image" uri into consideration
+ if (!srcValue.StartsWith("data:image"))
+ {
+ continue;
+ }
+
+ // Create tmp image by scanning the srcValue
+ // the value will look like "data:image/jpg;base64,abc" where the first part
+ // is the mimetype and the second (after the comma) is the image blob
+ Match dataUriInfo = Regex.Match(srcValue, @"^data:\w+\/(?\w+)[\w\+]*?;(?\w+),(?.+)$");
+
+ // If it turns up false, it was probably a false-positive and we can't do anything with it
+ if (dataUriInfo.Success is false)
+ {
+ continue;
+ }
+
+ var ext = dataUriInfo.Groups["ext"].Value.ToLowerInvariant();
+ var encoding = dataUriInfo.Groups["encoding"].Value.ToLowerInvariant();
+ var imageData = dataUriInfo.Groups["data"].Value;
+
+ if (_contentSettings.IsFileAllowedForUpload(ext) is false)
+ {
+ // If the image format is not supported we should probably leave it be
+ // since the user decided to include it.
+ // If we accepted it anyway, they could technically circumvent the allow list for file types,
+ // but the user experience would not be very good if we simply failed to save the content.
+ // Besides, there may be other types of data uri images technically supported by a browser that we cannot handle.
+ _logger.LogWarning(
+ "Performance impact: Could not convert embedded image to a Media item because the file extension {Ext} was not allowed. HTML extract: {OuterHtml}",
+ ext,
+ img.OuterHtml.Length < 100 ? img.OuterHtml : img.OuterHtml[..100]); // only log the first 100 chars because base64 images can be very long
+ continue;
+ }
+
+ // Create an unique folder path to help with concurrent users to avoid filename clash
+ var imageTempPath =
+ _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads + Path.DirectorySeparatorChar + Guid.NewGuid());
+
+ // Ensure image temp path exists
+ if (Directory.Exists(imageTempPath) is false)
+ {
+ Directory.CreateDirectory(imageTempPath);
+ }
+
+ // To get the filename, we simply manipulate the mimetype into a filename
+ var filePath = $"image.{ext}";
+ var safeFileName = filePath.ToSafeFileName(_shortStringHelper);
+ var tmpImgPath = imageTempPath + Path.DirectorySeparatorChar + safeFileName;
+ var absoluteTempImagePath = Path.GetFullPath(tmpImgPath);
+
+ // Convert the base64 content to a byte array and save the bytes directly to a file
+ // this method should work for most use-cases
+ if (encoding.Equals("base64"))
+ {
+ System.IO.File.WriteAllBytes(absoluteTempImagePath, Convert.FromBase64String(imageData));
+ }
+ else
+ {
+ System.IO.File.WriteAllText(absoluteTempImagePath, HttpUtility.HtmlDecode(imageData), Encoding.UTF8);
+ }
+
+ // When the temp file has been created, we can persist it
+ PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath);
+ }
+
+ return htmlDoc.DocumentNode.OuterHtml;
+ }
+
+ ///
+ /// Used by the RTE (and grid RTE) for drag/drop/persisting images.
+ ///
+ /// HTML from the Rich Text Editor property editor.
+ ///
+ ///
+ ///
+ /// Formatted HTML.
+ [Obsolete("It is not needed to supply the imageUrlGenerator parameter")]
+ public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator) =>
+ FindAndPersistPastedTempImages(html, mediaParentFolder, userId);
+
+ ///
+ /// Used by the RTE (and grid RTE) for drag/drop/persisting images.
+ ///
+ /// HTML from the Rich Text Editor property editor.
+ ///
+ ///
+ /// Formatted HTML.
+ public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
@@ -69,16 +224,11 @@ public sealed class RichTextEditorPastedImages
htmlDoc.LoadHtml(html);
HtmlNodeCollection? tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]");
- if (tmpImages == null || tmpImages.Count == 0)
+ if (tmpImages is null || tmpImages.Count is 0)
{
return html;
}
- // An array to contain a list of URLs that
- // we have already processed to avoid dupes
- var uploadedImages = new Dictionary();
-
-
foreach (HtmlNode? img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
@@ -89,116 +239,119 @@ public sealed class RichTextEditorPastedImages
continue;
}
+ var qualifiedTmpImgPath = _hostingEnvironment.MapPathContentRoot(tmpImgPath);
- var absoluteTempImagePath = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(tmpImgPath));
-
- if (IsValidPath(absoluteTempImagePath) == false)
- {
- continue;
- }
-
- var fileName = Path.GetFileName(absoluteTempImagePath);
- var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
-
- var mediaItemName = safeFileName.ToFriendlyName();
- IMedia mediaFile;
- GuidUdi udi;
-
- if (uploadedImages.ContainsKey(tmpImgPath) == false)
- {
- if (mediaParentFolder == Guid.Empty)
- {
- mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
- }
- else
- {
- mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
- }
-
- var fileInfo = new FileInfo(absoluteTempImagePath);
-
- FileStream? fileStream = fileInfo.OpenReadWithRetry();
- if (fileStream == null)
- {
- throw new InvalidOperationException("Could not acquire file stream");
- }
-
- using (fileStream)
- {
- mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
- }
-
- _mediaService.Save(mediaFile, userId);
-
- udi = mediaFile.GetUdi();
- }
- else
- {
- // Already been uploaded & we have it's UDI
- udi = uploadedImages[tmpImgPath];
- }
-
- // Add the UDI to the img element as new data attribute
- img.SetAttributeValue("data-udi", udi.ToString());
-
- // Get the new persisted image URL
- _umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
- IPublishedContent? mediaTyped = umbracoContext?.Media?.GetById(udi.Guid);
- if (mediaTyped == null)
- {
- throw new PanicException(
- $"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
- }
-
- var location = mediaTyped.Url(_publishedUrlProvider);
-
- // Find the width & height attributes as we need to set the imageprocessor QueryString
- var width = img.GetAttributeValue("width", int.MinValue);
- var height = img.GetAttributeValue("height", int.MinValue);
-
- if (width != int.MinValue && height != int.MinValue)
- {
- location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location)
- {
- ImageCropMode = ImageCropMode.Max,
- Width = width,
- Height = height,
- });
- }
-
- img.SetAttributeValue("src", location);
-
- // Remove the data attribute (so we do not re-process this)
- img.Attributes.Remove(TemporaryImageDataAttribute);
-
- // Add to the dictionary to avoid dupes
- if (uploadedImages.ContainsKey(tmpImgPath) == false)
- {
- uploadedImages.Add(tmpImgPath, udi);
-
- // Delete folder & image now its saved in media
- // The folder should contain one image - as a unique guid folder created
- // for each image uploaded from TinyMceController
- var folderName = Path.GetDirectoryName(absoluteTempImagePath);
- try
- {
- if (folderName is not null)
- {
- Directory.Delete(folderName, true);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
- }
- }
+ PersistMediaItem(mediaParentFolder, userId, img, qualifiedTmpImgPath);
}
return htmlDoc.DocumentNode.OuterHtml;
}
- private bool IsValidPath(string imagePath)
+ private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img, string qualifiedTmpImgPath)
{
- return imagePath.StartsWith(_tempFolderAbsolutePath);
+ var absoluteTempImagePath = Path.GetFullPath(qualifiedTmpImgPath);
+
+ if (IsValidPath(absoluteTempImagePath) is false)
+ {
+ return;
+ }
+
+ var fileName = Path.GetFileName(absoluteTempImagePath);
+ var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
+
+ var mediaItemName = safeFileName.ToFriendlyName();
+ GuidUdi udi;
+
+ if (_uploadedImages.ContainsKey(qualifiedTmpImgPath) is false)
+ {
+ var isSvg = qualifiedTmpImgPath.EndsWith(".svg");
+ var mediaType = isSvg
+ ? Constants.Conventions.MediaTypes.VectorGraphicsAlias
+ : Constants.Conventions.MediaTypes.Image;
+
+ IMedia mediaFile = mediaParentFolder == Guid.Empty
+ ? _mediaService.CreateMedia(mediaItemName, Constants.System.Root, mediaType, userId)
+ : _mediaService.CreateMedia(mediaItemName, mediaParentFolder, mediaType, userId);
+
+ var fileInfo = new FileInfo(absoluteTempImagePath);
+
+ FileStream? fileStream = fileInfo.OpenReadWithRetry();
+ if (fileStream is null)
+ {
+ throw new InvalidOperationException("Could not acquire file stream");
+ }
+
+ using (fileStream)
+ {
+ mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
+ _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
+ }
+
+ _mediaService.Save(mediaFile, userId);
+
+ udi = mediaFile.GetUdi();
+ }
+ else
+ {
+ // Already been uploaded & we have it's UDI
+ udi = _uploadedImages[qualifiedTmpImgPath];
+ }
+
+ // Add the UDI to the img element as new data attribute
+ img.SetAttributeValue("data-udi", udi.ToString());
+
+ // Get the new persisted image URL
+ _umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
+ IPublishedContent? mediaTyped = umbracoContext?.Media?.GetById(udi.Guid);
+ if (mediaTyped is null)
+ {
+ throw new PanicException(
+ $"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
+ }
+
+ var location = mediaTyped.Url(_publishedUrlProvider);
+
+ // Find the width & height attributes as we need to set the imageprocessor QueryString
+ var width = img.GetAttributeValue("width", int.MinValue);
+ var height = img.GetAttributeValue("height", int.MinValue);
+
+ if (width != int.MinValue && height != int.MinValue)
+ {
+ location = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location)
+ {
+ ImageCropMode = ImageCropMode.Max,
+ Width = width,
+ Height = height,
+ });
+ }
+
+ img.SetAttributeValue("src", location);
+
+ // Remove the data attribute (so we do not re-process this)
+ img.Attributes.Remove(TemporaryImageDataAttribute);
+
+ // Add to the dictionary to avoid dupes
+ if (_uploadedImages.ContainsKey(qualifiedTmpImgPath) is false)
+ {
+ _uploadedImages.Add(qualifiedTmpImgPath, udi);
+
+ // Delete folder & image now its saved in media
+ // The folder should contain one image - as a unique guid folder created
+ // for each image uploaded from TinyMceController
+ var folderName = Path.GetDirectoryName(absoluteTempImagePath);
+ try
+ {
+ if (folderName is not null)
+ {
+ Directory.Delete(folderName, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
+ }
+ }
}
+
+ private bool IsValidPath(string imagePath) => imagePath.StartsWith(_tempFolderAbsolutePath);
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index 85811a4c5e..8f59681afc 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -293,8 +293,10 @@ public class RichTextPropertyEditor : DataEditor
return null;
}
+ var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages(
+ editorValue.Value.ToString()!, mediaParentId, userId);
var parseAndSavedTempImages =
- _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString()!, mediaParentId, userId, _imageUrlGenerator);
+ _pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId);
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
var sanitized = _htmlSanitizer.Sanitize(parsed);
diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs
index 295c92a6d6..4455b01df3 100644
--- a/src/Umbraco.Infrastructure/Scoping/Scope.cs
+++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs
@@ -357,7 +357,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
}
- public void Dispose()
+ public override void Dispose()
{
EnsureNotDisposed();
@@ -402,16 +402,21 @@ namespace Umbraco.Cms.Infrastructure.Scoping
Completed = true;
}
- if (ParentScope != null)
- {
- ParentScope.ChildCompleted(Completed);
- }
- else
+ // CoreScope.Dispose will handle file systems and notifications, as well as notifying any parent scope of the child scope's completion.
+ // In this overridden class, we re-use that functionality and also handle scope context (including enlisted actions) and detached scopes.
+ // We retain order of events behaviour from Umbraco 11:
+ // - handle file systems (in CoreScope)
+ // - handle scoped notifications (in CoreScope)
+ // - handle scope context (in Scope)
+ // - handle detatched scopes (in Scope)
+ if (ParentScope is null)
{
DisposeLastScope();
}
-
- base.Dispose();
+ else
+ {
+ ParentScope.ChildCompleted(Completed);
+ }
_disposed = true;
}
@@ -559,6 +564,8 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
TryFinally(
+ HandleScopedFileSystems,
+ HandleScopedNotifications,
HandleScopeContext,
HandleDetachedScopes);
}
diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
index 90104b30a4..bfa65b3065 100644
--- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
+++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
@@ -3,7 +3,6 @@
Umbraco.Cms.Web.BackOfficeUmbraco CMS - Web - BackofficeContains the backoffice assembly needed to run the backend of Umbraco CMS.
- LibraryUmbraco.Cms.Web.BackOffice
diff --git a/src/Umbraco.Web.Common/UmbracoHelper.cs b/src/Umbraco.Web.Common/UmbracoHelper.cs
index 17729f3364..e59e4db588 100644
--- a/src/Umbraco.Web.Common/UmbracoHelper.cs
+++ b/src/Umbraco.Web.Common/UmbracoHelper.cs
@@ -1,4 +1,6 @@
+using System.Globalization;
using System.Xml.XPath;
+using Serilog.Events;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -139,12 +141,43 @@ public class UmbracoHelper
///
public string? GetDictionaryValue(string key) => CultureDictionary[key];
+
+ ///
+ /// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
+ ///
+ /// key of dictionary item
+ /// the specific culture on which the result well be back upon
+ ///
+ public string? GetDictionaryValue(string key, CultureInfo specificCulture)
+ {
+ _cultureDictionary = _cultureDictionaryFactory.CreateDictionary(specificCulture);
+ return GetDictionaryValue(key);
+ }
+
+ ///
+ /// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
+ ///
+ /// key of dictionary item
+ /// fall back text if dictionary item is empty - Name altText to match Umbraco.Field
+ ///
+ public string GetDictionaryValueOrDefault(string key, string defaultValue)
+ {
+ var dictionaryValue = GetDictionaryValue(key);
+ if (string.IsNullOrWhiteSpace(dictionaryValue))
+ {
+ dictionaryValue = defaultValue;
+ }
+
+ return dictionaryValue;
+ }
+
///
/// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
///
/// key of dictionary item
/// fall back text if dictionary item is empty - Name altText to match Umbraco.Field
///
+ [Obsolete("Use GetDictionaryValueOrDefault instead, scheduled for removal in v14.")]
public string GetDictionaryValue(string key, string altText)
{
var dictionaryValue = GetDictionaryValue(key);
@@ -156,6 +189,25 @@ public class UmbracoHelper
return dictionaryValue;
}
+ ///
+ /// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
+ ///
+ /// key of dictionary item
+ /// the specific culture on which the result well be back upon
+ /// fall back text if dictionary item is empty - Name altText to match Umbraco.Field
+ ///
+ public string GetDictionaryValueOrDefault(string key, CultureInfo specificCulture, string defaultValue)
+ {
+ _cultureDictionary = _cultureDictionaryFactory.CreateDictionary(specificCulture);
+ var dictionaryValue = GetDictionaryValue(key);
+ if (string.IsNullOrWhiteSpace(dictionaryValue))
+ {
+ dictionaryValue = defaultValue;
+ }
+ return dictionaryValue;
+ }
+
+
///
/// Returns the ICultureDictionary for access to dictionary items
///
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index 24ff76f74e..c088148f80 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -16470,9 +16470,9 @@
}
},
"node_modules/tough-cookie": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
- "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+ "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
index b453127613..cae4b803d8 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
@@ -3,11 +3,14 @@ angular.module("umbraco")
function ($scope, localizationService, $filter) {
var unsubscribe = [];
- var vm = this;
+ const vm = this;
vm.navigation = [];
- vm.filterSearchTerm = '';
+ vm.filter = {
+ searchTerm: ""
+ };
+
vm.filteredItems = [];
// Ensure groupKey value, as we need it to be present for the filtering logic.
@@ -15,12 +18,19 @@ angular.module("umbraco")
item.blockConfigModel.groupKey = item.blockConfigModel.groupKey || null;
});
- unsubscribe.push($scope.$watch('vm.filterSearchTerm', updateFiltering));
+ unsubscribe.push($scope.$watch('vm.filter.searchTerm', updateFiltering));
function updateFiltering() {
- vm.filteredItems = $filter('umbCmsBlockCard')($scope.model.availableItems, vm.filterSearchTerm);
+ vm.filteredItems = $filter('umbCmsBlockCard')($scope.model.availableItems, vm.filter.searchTerm);
}
+ vm.filterByGroup = function (group) {
+
+ const items = $filter('filter')(vm.filteredItems, { blockConfigModel: { groupKey: group?.key || null } });
+
+ return items;
+ };
+
localizationService.localizeMany(["blockEditor_tabCreateEmpty", "blockEditor_tabClipboard"]).then(
function (data) {
@@ -47,9 +57,7 @@ angular.module("umbraco")
} else {
vm.activeTab = vm.navigation[0];
}
-
-
-
+
vm.activeTab.active = true;
}
);
@@ -61,12 +69,13 @@ angular.module("umbraco")
};
vm.clickClearClipboard = function () {
- vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually.
+ vm.model.clipboardItems = []; // This dialog is not connected via the clipboardService events, so we need to update manually.
vm.model.clickClearClipboard();
+
if (vm.model.singleBlockMode !== true && vm.model.openClipboard !== true)
{
vm.onNavigationChanged(vm.navigation[0]);
- vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here.
+ vm.navigation[1].disabled = true; // disabled ws determined when creating the navigation, so we need to update it here.
}
else {
vm.close();
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html
index 2a84fad343..b7b38797da 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html
@@ -19,7 +19,7 @@