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}}"> -
- - @@ -46,11 +46,13 @@
-
-
- +
+ +
+ + - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index 93d4398125..63ab76b553 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -51,13 +51,13 @@ vm.requestRemoveBlockByIndex = function (index, event) { - const labelKeys = [ - "general_delete", - "blockEditor_confirmDeleteBlockTypeMessage", - "blockEditor_confirmDeleteBlockTypeNotice" - ]; + const labelKeys = [ + "general_delete", + "blockEditor_confirmDeleteBlockTypeMessage", + "blockEditor_confirmDeleteBlockTypeNotice" + ]; - localizationService.localizeMany(labelKeys).then(data => { + localizationService.localizeMany(labelKeys).then(data => { var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ebb17d6046..bc6e2e13b6 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,6 +1,7 @@ Umbraco.Cms.Web.UI + false false diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index f9f3779b47..31c057e7b9 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.Web.Website Umbraco CMS - Web - Website Contains the website assembly needed to run the frontend of Umbraco CMS. - Library Umbraco.Cms.Web.Website diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index a773c6a1c9..ec220ec3a8 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,10 +2,15 @@ + + + false + false + + annotations - false diff --git a/tests/Umbraco.TestData/Umbraco.TestData.csproj b/tests/Umbraco.TestData/Umbraco.TestData.csproj index 64772d371e..fd5335ecc6 100644 --- a/tests/Umbraco.TestData/Umbraco.TestData.csproj +++ b/tests/Umbraco.TestData/Umbraco.TestData.csproj @@ -1,10 +1,4 @@ - - Umbraco.TestData - false - false - - diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index e24c03c2c3..f52849d19e 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,9 +1,6 @@ Exe - false - false - false diff --git a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs index da55c37668..61504bf069 100644 --- a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs @@ -95,7 +95,6 @@ public class LanguageBuilder return this; } - [Obsolete("This will be replaced in V13 by a corresponding method accepting language ISO code instead of language ID.")] public LanguageBuilder WithFallbackLanguageIsoCode(string fallbackLanguageIsoCode) { _fallbackLanguageIsoCode = fallbackLanguageIsoCode; diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index 835bde8848..26d1baf85b 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -4,6 +4,7 @@ Umbraco CMS - Test tools Contains commonly used tools to write tests for Umbraco CMS, such as various builders for content etc. Umbraco.Cms.Tests.Common + true true diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 145300ef37..da08d8833f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -1,11 +1,11 @@ + true Umbraco.Cms.Tests.Integration Umbraco CMS - Integration tests Contains helper classes for integration tests with Umbraco CMS, including all internal integration tests. - true - true Umbraco.Cms.Tests.Integration + true true diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs index b28cd17b62..f83e5e0190 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs @@ -705,4 +705,87 @@ public class ContentControllerTests : UmbracoTestServerTestBase Assert.AreEqual(0, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); }); } + + [TestCase( + @"

", + false)] + [TestCase( + @"

"">

", + false)] + [TestCase( + @"

", + false)] + [TestCase( + @"

", + true)] + public async Task PostSave_Simple_RichText_With_Base64(string html, bool shouldHaveDataUri) + { + var url = PrepareApiControllerUrl(x => x.PostSave(null)); + + var dataTypeService = GetRequiredService(); + var contentService = GetRequiredService(); + var contentTypeService = GetRequiredService(); + + var dataType = new DataTypeBuilder() + .WithId(0) + .WithoutIdentity() + .WithDatabaseType(ValueStorageType.Ntext) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.TinyMce) + .Done() + .Build(); + + dataTypeService.Save(dataType); + + var contentType = new ContentTypeBuilder() + .WithId(0) + .AddPropertyType() + .WithDataTypeId(dataType.Id) + .WithAlias("richText") + .WithName("Rich Text") + .Done() + .WithContentVariation(ContentVariation.Nothing) + .Build(); + + contentTypeService.Save(contentType); + + var content = new ContentBuilder() + .WithId(0) + .WithName("Invariant") + .WithContentType(contentType) + .AddPropertyData() + .WithKeyValue("richText", html) + .Done() + .Build(); + contentService.SaveAndPublish(content); + var model = new ContentItemSaveBuilder() + .WithContent(content) + .Build(); + + // Act + var response = + await Client.PostAsync(url, new MultipartFormDataContent {{new StringContent(JsonConvert.SerializeObject(model)), "contentItem"}}); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + + Assert.Multiple(() => + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, body); + var display = JsonConvert.DeserializeObject(body); + var bodyText = display.Variants.FirstOrDefault()?.Tabs.FirstOrDefault()?.Properties + ?.FirstOrDefault(x => x.Alias.Equals("richText"))?.Value?.ToString(); + Assert.NotNull(bodyText); + + var containsDataUri = bodyText.Contains("data:image"); + if (shouldHaveDataUri) + { + Assert.True(containsDataUri, $"Data URIs were expected to be found in the body: {bodyText}"); + } else { + Assert.False(containsDataUri, $"Data URIs were not expected to be found in the body: {bodyText}"); + } + }); + } } diff --git a/tests/Umbraco.Tests.Integration/appsettings.Tests.json b/tests/Umbraco.Tests.Integration/appsettings.Tests.json index 101b1a1aef..1d4bbf18ef 100644 --- a/tests/Umbraco.Tests.Integration/appsettings.Tests.json +++ b/tests/Umbraco.Tests.Integration/appsettings.Tests.json @@ -1,4 +1,5 @@ { + "$schema": "./appsettings-schema.json", "Logging": { "LogLevel": { "Default": "Warning", @@ -16,5 +17,12 @@ "EmptyDatabasesCount": 2, "SQLServerMasterConnectionString": "" } + }, + "Umbraco": { + "CMS": { + "Content": { + "AllowedUploadedFileExtensions": ["jpg", "png", "gif", "svg"] + } + } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/TopoGraphTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/TopoGraphTests.cs new file mode 100644 index 0000000000..ce54c6e762 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/TopoGraphTests.cs @@ -0,0 +1,126 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Collections; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Collections; + +[TestFixture] +public class TopoGraphTests +{ + [Test] + public void CycleTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }.Depends(3)); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + try + { + var ordered = graph.GetSortedItems().ToArray(); + Assert.Fail("Expected: Exception."); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.StartsWith(TopoGraph.CycleDependencyError)); + } + } + + [Test] + public void IgnoreCycleTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }.Depends(3)); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + var ordered = graph.GetSortedItems(throwOnCycle: false).ToArray(); + + // default order is dependencies before item + Assert.AreEqual(2, ordered[0].Id); // ignored cycle + Assert.AreEqual(3, ordered[1].Id); + Assert.AreEqual(1, ordered[2].Id); + } + + [Test] + public void MissingTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }.Depends(4)); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + try + { + var ordered = graph.GetSortedItems().ToArray(); + Assert.Fail("Expected: Exception."); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.StartsWith(TopoGraph.MissingDependencyError)); + } + } + + [Test] + public void IgnoreMissingTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }.Depends(4)); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + var ordered = graph.GetSortedItems(throwOnMissing: false).ToArray(); + + // default order is dependencies before item + Assert.AreEqual(1, ordered[0].Id); // ignored dependency + Assert.AreEqual(2, ordered[1].Id); + Assert.AreEqual(3, ordered[2].Id); + } + + [Test] + public void OrderTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + var ordered = graph.GetSortedItems().ToArray(); + + // default order is dependencies before item + Assert.AreEqual(1, ordered[0].Id); + Assert.AreEqual(2, ordered[1].Id); + Assert.AreEqual(3, ordered[2].Id); + } + + [Test] + public void ReverseTest() + { + var graph = new TopoGraph(x => x.Id, x => x.Dependencies); + graph.AddItem(new Thing { Id = 1, Name = "One" }); + graph.AddItem(new Thing { Id = 2, Name = "Two" }.Depends(1)); + graph.AddItem(new Thing { Id = 3, Name = "Three" }.Depends(2)); + + var ordered = graph.GetSortedItems(reverse: true).ToArray(); + + // reverse order is item before dependencies + Assert.AreEqual(3, ordered[0].Id); + Assert.AreEqual(2, ordered[1].Id); + Assert.AreEqual(1, ordered[2].Id); + } + + public class Thing + { + public int Id { get; set; } + + public string Name { get; set; } + + public List Dependencies { get; } = new List(); + + public Thing Depends(params int[] dependencies) + { + Dependencies.AddRange(dependencies); + return this; + } + } + +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index bb58a6f090..72fb63adcd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -2,8 +2,6 @@ true Umbraco.Cms.Tests.UnitTests - false - false