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/.gitmodules b/.gitmodules
index 2106b81d2f..e69de29bb2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "src/Umbraco.Web.UI.New.Client"]
- path = src/Umbraco.Web.UI.New.Client
- url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git
diff --git a/Directory.Build.props b/Directory.Build.props
index 087cea7abc..50ec727fc0 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -29,9 +29,9 @@
- true
- false
- 12.0.0-rc1
+ false
+ true
+ 12.0.0
true
true
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 4c14cb084b..7c94cc91e1 100644
--- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
+++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
@@ -2,10 +2,6 @@
Umbraco CMS - API Common
Contains the bits and pieces that are shared between the Umbraco CMS APIs.
- true
- false
- Umbraco.Cms.Api.Common
- Umbraco.Cms.Api.Common
@@ -19,6 +15,7 @@
+
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 API
Contains 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 2
Adds 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 d1939fda24..99ebc5306d 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 4191bdb9ea..c87691a4ec 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.
@@ -21,5 +20,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 e8ceb6b216..d9799164ed 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.
false
false
-
- false
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 541e479d45..01dea9b4ef 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/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs
index 0f0d2c3d0b..ffc1d3874a 100644
--- a/src/Umbraco.Core/Models/DictionaryItem.cs
+++ b/src/Umbraco.Core/Models/DictionaryItem.cs
@@ -34,6 +34,9 @@ public class DictionaryItem : EntityBase, IDictionaryItem
_translations = new List();
}
+ [Obsolete("This will be removed in V14.")]
+ public Func? GetLanguage { get; set; }
+
///
/// Gets or Sets the Parent Id of the Dictionary Item
///
diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs
index ff63cd7f9c..02e6736654 100644
--- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs
+++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs
@@ -5,10 +5,14 @@ namespace Umbraco.Cms.Core.Models;
public interface IDictionaryTranslation : IEntity, IRememberBeingDirty
{
+ ///
+ /// Gets the ISO code of the language.
+ ///
+ [DataMember]
string LanguageIsoCode { get; }
///
- /// Gets or sets the translated text
+ /// Gets or sets the translated text.
///
[DataMember]
string Value { get; set; }
diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs
index b8ea8e132e..efed3314df 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;
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/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index ed4903f531..fb901ece46 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -87,7 +87,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;
@@ -106,7 +106,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)
{
@@ -166,7 +166,7 @@ public class DatabaseSchemaCreator
var dataCreation = new DatabaseDataCreator(
_database, _loggerFactory.CreateLogger(),
_umbracoVersion,
- _defaultDataCreationSettings);
+ _installDefaultDataSettings);
foreach (Type table in _orderedTables)
{
CreateTable(false, table, dataCreation);
@@ -443,7 +443,7 @@ public class DatabaseSchemaCreator
_database,
_loggerFactory.CreateLogger(),
_umbracoVersion,
- _defaultDataCreationSettings));
+ _installDefaultDataSettings));
}
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
index 4fc868256b..1259fb2b3d 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.FindAndPersistPastedTempImages(
+ html, mediaParentId, userId, _imageUrlGenerator);
+ var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId, _imageUrlGenerator);
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 264602897b..0c195d4f8d 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
@@ -105,7 +105,7 @@ public sealed class RichTextEditorPastedImages
}
///
- /// Used by the RTE (and grid RTE) for drag/drop/persisting images
+ /// Used by the RTE (and grid RTE) for drag/drop/persisting images.
///
public async Task FindAndPersistPastedTempImagesAsync(string html, Guid mediaParentFolder, Guid userKey, IImageUrlGenerator imageUrlGenerator)
{
@@ -115,7 +115,7 @@ 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;
}
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.BackOffice
Umbraco CMS - Web - Backoffice
Contains the backoffice assembly needed to run the backend of Umbraco CMS.
- Library
Umbraco.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 d1a87ff3b2..8cf713824e 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 @@
-
+
@@ -39,14 +39,14 @@
-
+
{{blockGroup.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
index 85c31dfc6b..ade5e9829a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
@@ -114,22 +114,29 @@
});
}
- vm.requestRemoveBlockByIndex = function (index) {
- localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockTypeMessage", "blockEditor_confirmDeleteBlockTypeNotice"]).then(function (data) {
+ vm.requestRemoveBlockByIndex = function (index, event) {
+
+ const labelKeys = [
+ "general_delete",
+ "blockEditor_confirmDeleteBlockTypeMessage",
+ "blockEditor_confirmDeleteBlockTypeNotice"
+ ];
+
+ localizationService.localizeMany(labelKeys).then(data => {
var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey);
overlayService.confirmDelete({
title: data[0],
content: localizationService.tokenReplace(data[1], [contentElementType ? contentElementType.name : "(Unavailable ElementType)"]),
confirmMessage: data[2],
- close: function () {
- overlayService.close();
- },
- submit: function () {
+ submit: () => {
vm.removeBlockByIndex(index);
overlayService.close();
- }
+ },
+ close: overlayService.close()
});
});
+
+ event.stopPropagation();
}
vm.removeBlockByIndex = function (index) {
@@ -164,7 +171,7 @@
placeholder: '--sortable-placeholder',
forcePlaceHolderSize: true,
stop: function(e, ui) {
- if(ui.item.sortable.droptarget && ui.item.sortable.droptarget.length > 0) {
+ if (ui.item.sortable.droptarget && ui.item.sortable.droptarget.length > 0) {
// We do not want sortable to actually move the data, as we are using the same ng-model. Instead we just change the groupKey and cancel the transfering.
ui.item.sortable.model.groupKey = ui.item.sortable.droptarget[0].dataset.groupKey || null;
ui.item.sortable.cancel();
@@ -346,7 +353,7 @@
// Then remove group:
const groupIndex = vm.blockGroups.indexOf(blockGroup);
- if(groupIndex !== -1) {
+ if (groupIndex !== -1) {
vm.blockGroups.splice(groupIndex, 1);
removeReferencesToGroupKey(blockGroup.key);
}
@@ -375,7 +382,7 @@
const groupName = "Demo Blocks";
var sampleGroup = vm.blockGroups.find(x => x.name === groupName);
- if(!sampleGroup) {
+ if (!sampleGroup) {
sampleGroup = {
key: String.CreateGuid(),
name: groupName
@@ -394,6 +401,7 @@
initSampleBlock(data.umbBlockGridDemoHeadlineBlock, sampleGroup.key, {"label": "Headline ({{headline | truncate:true:36}})", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoHeadlineBlock.html"});
initSampleBlock(data.umbBlockGridDemoImageBlock, sampleGroup.key, {"label": "Image", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html"});
initSampleBlock(data.umbBlockGridDemoRichTextBlock, sampleGroup.key, { "label": "Rich Text ({{richText | ncRichText | truncate:true:36}})", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html"});
+
const twoColumnLayoutAreas = [
{
'key': String.CreateGuid(),
@@ -414,6 +422,7 @@
'specifiedAllowance': []
}
];
+
initSampleBlock(data.umbBlockGridDemoTwoColumnLayoutBlock, sampleGroup.key, {"label": "Two Column Layout", "view": "~/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoTwoColumnLayoutBlock.html", "allowInAreas": false, "areas": twoColumnLayoutAreas});
vm.showSampleDataCTA = false;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.html
index e78d94d486..a30ef4e8db 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.html
@@ -19,12 +19,12 @@
ng-class="{'--isOpen':vm.openBlock === block}"
ng-click="vm.openBlockOverlay(block)"
data-content-element-type-key="{{block.contentElementTypeKey}}">
-
-