diff --git a/src/Umbraco.Core/Models/IDomain.cs b/src/Umbraco.Core/Models/IDomain.cs index 2d4845c9a6..edfff6512e 100644 --- a/src/Umbraco.Core/Models/IDomain.cs +++ b/src/Umbraco.Core/Models/IDomain.cs @@ -2,18 +2,58 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models; +/// +/// Represents a domain name, optionally assigned to a content and/or language ID. +/// +/// +/// public interface IDomain : IEntity, IRememberBeingDirty { - int? LanguageId { get; set; } - + /// + /// Gets or sets the name of the domain. + /// + /// + /// The name of the domain. + /// string DomainName { get; set; } - int? RootContentId { get; set; } - + /// + /// Gets a value indicating whether this is a wildcard domain (only specifying the language of a content node). + /// + /// + /// true if this is a wildcard domain; otherwise, false. + /// bool IsWildcard { get; } /// - /// Readonly value of the language ISO code for the domain + /// Gets or sets the language ID assigned to the domain. /// + /// + /// The language ID assigned to the domain. + /// + int? LanguageId { get; set; } + + /// + /// Gets the language ISO code. + /// + /// + /// The language ISO code. + /// string? LanguageIsoCode { get; } + + /// + /// Gets or sets the root content ID assigned to the domain. + /// + /// + /// The root content ID assigned to the domain. + /// + int? RootContentId { get; set; } + + /// + /// Gets or sets the sort order. + /// + /// + /// The sort order. + /// + int SortOrder { get => IsWildcard ? -1 : 0; set { } } // TODO Remove default implementation in a future version } diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs index 2c665f1710..e6df70621a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs @@ -3,56 +3,82 @@ using System.Collections; namespace Umbraco.Cms.Core.Models.PublishedContent; /// -/// Manages the built-in fallback policies. +/// Manages the built-in fallback policies. /// public struct Fallback : IEnumerable { /// - /// Do not fallback. + /// Do not fallback. /// public const int None = 0; private readonly int[] _values; /// - /// Initializes a new instance of the struct with values. + /// Initializes a new instance of the struct with values. /// + /// The values. private Fallback(int[] values) => _values = values; /// - /// Gets an ordered set of fallback policies. + /// Gets an ordered set of fallback policies. /// - /// + /// The values. + /// + /// The fallback policy. + /// public static Fallback To(params int[] values) => new(values); /// - /// Fallback to default value. + /// Fallback to the default value. /// public const int DefaultValue = 1; /// - /// Fallback to other languages. + /// Fallback to other languages. /// public const int Language = 2; /// - /// Fallback to tree ancestors. + /// Fallback to tree ancestors. /// public const int Ancestors = 3; /// - /// Gets the fallback to default value policy. + /// Fallback to the default language. /// + public const int DefaultLanguage = 4; + + /// + /// Gets the fallback to the default language policy. + /// + /// + /// The default language fallback policy. + /// + public static Fallback ToDefaultLanguage => new Fallback(new[] { DefaultLanguage }); + + /// + /// Gets the fallback to the default value policy. + /// + /// + /// The default value fallback policy. + /// public static Fallback ToDefaultValue => new(new[] { DefaultValue }); /// - /// Gets the fallback to language policy. + /// Gets the fallback to language policy. /// + /// + /// The language fallback policy. + /// public static Fallback ToLanguage => new(new[] { Language }); /// - /// Gets the fallback to tree ancestors policy. + /// Gets the fallback to tree ancestors policy. /// + /// + /// The tree ancestors fallback policy. + /// public static Fallback ToAncestors => new(new[] { Ancestors }); /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index 64f0160383..0e43a2617f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -44,6 +44,13 @@ public class PublishedValueFallback : IPublishedValueFallback return true; } + break; + case Fallback.DefaultLanguage: + if (TryGetValueWithDefaultLanguageFallback(property, culture, segment, out value)) + { + return true; + } + break; default: throw NotSupportedFallbackMethod(f, "property"); @@ -85,6 +92,13 @@ public class PublishedValueFallback : IPublishedValueFallback return true; } + break; + case Fallback.DefaultLanguage: + if (TryGetValueWithDefaultLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + break; default: throw NotSupportedFallbackMethod(f, "element"); @@ -141,6 +155,13 @@ public class PublishedValueFallback : IPublishedValueFallback return true; } + break; + case Fallback.DefaultLanguage: + if (TryGetValueWithDefaultLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + break; default: throw NotSupportedFallbackMethod(f, "content"); @@ -347,4 +368,42 @@ public class PublishedValueFallback : IPublishedValueFallback language = language2; } } + + private bool TryGetValueWithDefaultLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; + } + + string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); + if (culture.InvariantEquals(defaultCulture) == false && property.HasValue(defaultCulture, segment)) + { + value = property.Value(this, defaultCulture, segment); + return true; + } + + return false; + } + + private bool TryGetValueWithDefaultLanguageFallback(IPublishedElement element, string alias, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; + } + + string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); + if (culture.InvariantEquals(defaultCulture) == false && element.HasValue(alias, defaultCulture, segment)) + { + value = element.Value(this, alias, defaultCulture, segment); + return true; + } + + return false; + } } diff --git a/src/Umbraco.Core/Models/UmbracoDomain.cs b/src/Umbraco.Core/Models/UmbracoDomain.cs index c883e14770..f9c3cd753d 100644 --- a/src/Umbraco.Core/Models/UmbracoDomain.cs +++ b/src/Umbraco.Core/Models/UmbracoDomain.cs @@ -3,27 +3,33 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models; +/// [Serializable] [DataContract(IsReference = true)] public class UmbracoDomain : EntityBase, IDomain { - private int? _contentId; private string _domainName; private int? _languageId; + private int? _rootContentId; + private int _sortOrder; - public UmbracoDomain(string domainName) => _domainName = domainName; + /// + /// Initializes a new instance of the class. + /// + /// The name of the domain. + public UmbracoDomain(string domainName) + => _domainName = domainName; + /// + /// Initializes a new instance of the class. + /// + /// The name of the domain. + /// The language ISO code. public UmbracoDomain(string domainName, string languageIsoCode) - : this(domainName) => - LanguageIsoCode = languageIsoCode; - - [DataMember] - public int? LanguageId - { - get => _languageId; - set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); - } + : this(domainName) + => LanguageIsoCode = languageIsoCode; + /// [DataMember] public string DomainName { @@ -31,17 +37,33 @@ public class UmbracoDomain : EntityBase, IDomain set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName)); } + /// + public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); + + /// + [DataMember] + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); + } + + /// + public string? LanguageIsoCode { get; set; } + + /// [DataMember] public int? RootContentId { - get => _contentId; - set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(RootContentId)); + get => _rootContentId; + set => SetPropertyValueAndDetectChanges(value, ref _rootContentId, nameof(RootContentId)); } - public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); - - /// - /// Readonly value of the language ISO code for the domain - /// - public string? LanguageIsoCode { get; set; } + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } } diff --git a/src/Umbraco.Core/Routing/Domain.cs b/src/Umbraco.Core/Routing/Domain.cs index 291d7beed9..8c77b5661b 100644 --- a/src/Umbraco.Core/Routing/Domain.cs +++ b/src/Umbraco.Core/Routing/Domain.cs @@ -1,29 +1,44 @@ namespace Umbraco.Cms.Core.Routing; /// -/// Represents a published snapshot domain. +/// Represents a published snapshot domain. /// public class Domain { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the domain. /// The name of the domain. /// The identifier of the content which supports the domain. /// The culture of the domain. /// A value indicating whether the domain is a wildcard domain. + [Obsolete("Use the constructor specifying all properties instead. This constructor will be removed in a future version.")] public Domain(int id, string name, int contentId, string? culture, bool isWildcard) + : this(id, name, contentId, culture, isWildcard, -1) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the domain. + /// The name of the domain. + /// The identifier of the content which supports the domain. + /// The culture of the domain. + /// A value indicating whether the domain is a wildcard domain. + /// The sort order. + public Domain(int id, string name, int contentId, string? culture, bool isWildcard, int sortOrder) { Id = id; Name = name; ContentId = contentId; Culture = culture; IsWildcard = isWildcard; + SortOrder = sortOrder; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// An origin domain. protected Domain(Domain domain) @@ -33,30 +48,54 @@ public class Domain ContentId = domain.ContentId; Culture = domain.Culture; IsWildcard = domain.IsWildcard; + SortOrder = domain.SortOrder; } /// - /// Gets the unique identifier of the domain. + /// Gets the unique identifier of the domain. /// + /// + /// The unique identifier of the domain. + /// public int Id { get; } /// - /// Gets the name of the domain. + /// Gets the name of the domain. /// + /// + /// The name of the domain. + /// public string Name { get; } /// - /// Gets the identifier of the content which supports the domain. + /// Gets the identifier of the content which supports the domain. /// + /// + /// The identifier of the content which supports the domain. + /// public int ContentId { get; } /// - /// Gets the culture of the domain. + /// Gets the culture of the domain. /// + /// + /// The culture of the domain. + /// public string? Culture { get; } /// - /// Gets a value indicating whether the domain is a wildcard domain. + /// Gets a value indicating whether the domain is a wildcard domain. /// + /// + /// true if this is a wildcard domain; otherwise, false. + /// public bool IsWildcard { get; } + + /// + /// Gets the sort order. + /// + /// + /// The sort order. + /// + public int SortOrder { get; } } diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index f31244d2ac..7fe5017948 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; @@ -172,14 +173,10 @@ namespace Umbraco.Cms.Core.Routing // sanitize the list to have proper uris for comparison (scheme, path end with /) // we need to end with / because example.com/foo cannot match example.com/foobar // we need to order so example.com/foo matches before example.com/ - var domainsAndUris = domains? - .Where(d => d.IsWildcard == false) - .Select(d => new DomainAndUri(d, uri)) - .OrderByDescending(d => d.Uri.ToString()) - .ToList(); + DomainAndUri[]? domainsAndUris = SelectDomains(domains, uri)?.ToArray(); // nothing = no magic, return null - if (domainsAndUris is null || domainsAndUris.Count == 0) + if (domainsAndUris is null || domainsAndUris.Length == 0) { return null; } @@ -204,8 +201,9 @@ namespace Umbraco.Cms.Core.Routing IReadOnlyCollection considerForBaseDomains = domainsAndUris; if (cultureDomains != null) { - if (cultureDomains.Count == 1) // only 1, return + if (cultureDomains.Count == 1) { + // only 1, return return cultureDomains.First(); } @@ -214,9 +212,11 @@ namespace Umbraco.Cms.Core.Routing } // look for domains that would be the base of the uri - IReadOnlyCollection baseDomains = SelectByBase(considerForBaseDomains, uri, culture); - if (baseDomains.Count > 0) // found, return + // we need to order so example.com/foo matches before example.com/ + IReadOnlyCollection baseDomains = SelectByBase(considerForBaseDomains.OrderByDescending(d => d.Uri.ToString()).ToList(), uri, culture); + if (baseDomains.Count > 0) { + // found, return return baseDomains.First(); } @@ -246,9 +246,9 @@ namespace Umbraco.Cms.Core.Routing // if none matches, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com - Uri currentWithoutPort = currentWithSlash.WithoutPort(); if (baseDomains.Count == 0) { + Uri currentWithoutPort = currentWithSlash.WithoutPort(); baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithoutPort)).ToList(); } @@ -258,9 +258,9 @@ namespace Umbraco.Cms.Core.Routing private static IReadOnlyCollection? SelectByCulture(IReadOnlyCollection domainsAndUris, string? culture, string? defaultCulture) { // we try our best to match cultures, but may end with a bogus domain - - if (culture != null) // try the supplied culture + if (culture is not null) { + // try the supplied culture var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(culture)).ToList(); if (cultureDomains.Count > 0) { @@ -268,8 +268,9 @@ namespace Umbraco.Cms.Core.Routing } } - if (defaultCulture != null) // try the defaultCulture culture + if (defaultCulture is not null) { + // try the defaultCulture culture var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(defaultCulture)).ToList(); if (cultureDomains.Count > 0) { @@ -280,31 +281,32 @@ namespace Umbraco.Cms.Core.Routing return null; } - private static DomainAndUri GetByCulture(IReadOnlyCollection domainsAndUris, string? culture, string? defaultCulture) + private static DomainAndUri? GetByCulture(IReadOnlyCollection domainsAndUris, string? culture, string? defaultCulture) { DomainAndUri? domainAndUri; // we try our best to match cultures, but may end with a bogus domain - - if (culture != null) // try the supplied culture + if (culture is not null) { + // try the supplied culture domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture)); - if (domainAndUri != null) + if (domainAndUri is not null) { return domainAndUri; } } - if (defaultCulture != null) // try the defaultCulture culture + if (defaultCulture is not null) { + // try the defaultCulture culture domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture)); - if (domainAndUri != null) + if (domainAndUri is not null) { return domainAndUri; } } - return domainsAndUris.First(); // what else? + return domainsAndUris.FirstOrDefault(); } /// @@ -313,14 +315,10 @@ namespace Umbraco.Cms.Core.Routing /// The domains. /// The uri, or null. /// The domains and their normalized uris, that match the specified uri. - internal static IEnumerable SelectDomains(IEnumerable domains, Uri uri) - { + [return: NotNullIfNotNull(nameof(domains))] + private static IEnumerable? SelectDomains(IEnumerable? domains, Uri uri) // TODO: where are we matching ?!!? - return domains - .Where(d => d.IsWildcard == false) - .Select(d => new DomainAndUri(d, uri)) - .OrderByDescending(d => d.Uri.ToString()); - } + => domains?.Where(d => d.IsWildcard == false).Select(d => new DomainAndUri(d, uri)); /// /// Parses a domain name into a URI. @@ -351,9 +349,7 @@ namespace Umbraco.Cms.Core.Routing /// A value indicating if there is another domain defined down in the path. /// Looks _under_ rootNodeId but not _at_ rootNodeId. internal static bool ExistsDomainInPath(IEnumerable domains, string path, int? rootNodeId) - { - return FindDomainInPath(domains, path, rootNodeId) != null; - } + => FindDomainInPath(domains, path, rootNodeId) is not null; /// /// Gets the deepest non-wildcard Domain, if any, from a group of Domains, in a node path. @@ -364,17 +360,7 @@ namespace Umbraco.Cms.Core.Routing /// The deepest non-wildcard Domain in the path, or null. /// Looks _under_ rootNodeId but not _at_ rootNodeId. internal static Domain? FindDomainInPath(IEnumerable domains, string path, int? rootNodeId) - { - var stopNodeId = rootNodeId ?? -1; - - return path.Split(Constants.CharArrays.Comma) - .Reverse() - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)) - .TakeWhile(id => id != stopNodeId) - .Select(id => domains.FirstOrDefault(d => d.ContentId == id && d.IsWildcard == false)) - .SkipWhile(domain => domain == null) - .FirstOrDefault(); - } + => FindDomainInPath(domains, path, rootNodeId, false); /// /// Gets the deepest wildcard Domain, if any, from a group of Domains, in a node path. @@ -385,6 +371,9 @@ namespace Umbraco.Cms.Core.Routing /// The deepest wildcard Domain in the path, or null. /// Looks _under_ rootNodeId but not _at_ rootNodeId. public static Domain? FindWildcardDomainInPath(IEnumerable? domains, string path, int? rootNodeId) + => FindDomainInPath(domains, path, rootNodeId, true); + + private static Domain? FindDomainInPath(IEnumerable? domains, string path, int? rootNodeId, bool isWildcard) { var stopNodeId = rootNodeId ?? -1; @@ -392,8 +381,8 @@ namespace Umbraco.Cms.Core.Routing .Reverse() .Select(s => int.Parse(s, CultureInfo.InvariantCulture)) .TakeWhile(id => id != stopNodeId) - .Select(id => domains?.FirstOrDefault(d => d.ContentId == id && d.IsWildcard)) - .FirstOrDefault(domain => domain != null); + .Select(id => domains?.FirstOrDefault(d => d.ContentId == id && d.IsWildcard == isWildcard)) + .FirstOrDefault(domain => domain is not null); } /// diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index 38f27bb94c..202d51d648 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -16,8 +16,8 @@ public class DomainService : RepositoryService, IDomainService ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IDomainRepository domainRepository) - : base(provider, loggerFactory, eventMessagesFactory) => - _domainRepository = domainRepository; + : base(provider, loggerFactory, eventMessagesFactory) + => _domainRepository = domainRepository; public bool Exists(string domainName) { @@ -43,8 +43,7 @@ public class DomainService : RepositoryService, IDomainService _domainRepository.Delete(domain); scope.Complete(); - scope.Notifications.Publish( - new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification)); } return OperationResult.Attempt.Succeed(eventMessages); @@ -97,8 +96,50 @@ public class DomainService : RepositoryService, IDomainService _domainRepository.Save(domainEntity); scope.Complete(); - scope.Notifications.Publish( - new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification)); + + scope.Notifications.Publish(new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification)); + } + + return OperationResult.Attempt.Succeed(eventMessages); + } + + public Attempt Sort(IEnumerable items) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + + IDomain[] domains = items.ToArray(); + if (domains.Length == 0) + { + return OperationResult.Attempt.NoOperation(eventMessages); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var savingNotification = new DomainSavingNotification(domains, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(eventMessages); + } + + scope.WriteLock(Constants.Locks.Domains); + + int sortOrder = 0; + foreach (IDomain domain in domains) + { + // If the current sort order equals that of the domain we don't need to update it, so just increment the sort order and continue + if (domain.SortOrder == sortOrder) + { + sortOrder++; + continue; + } + + domain.SortOrder = sortOrder++; + _domainRepository.Save(domain); + } + + scope.Complete(); + scope.Notifications.Publish(new DomainSavedNotification(domains, eventMessages).WithStateFrom(savingNotification)); } return OperationResult.Attempt.Succeed(eventMessages); diff --git a/src/Umbraco.Core/Services/IDomainService.cs b/src/Umbraco.Core/Services/IDomainService.cs index 54a006ecb1..3b7cd29f80 100644 --- a/src/Umbraco.Core/Services/IDomainService.cs +++ b/src/Umbraco.Core/Services/IDomainService.cs @@ -1,3 +1,4 @@ +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -17,4 +18,7 @@ public interface IDomainService : IService IEnumerable GetAssignedDomains(int contentId, bool includeWildcards); Attempt Save(IDomain domainEntity); + + Attempt Sort(IEnumerable items) + => Attempt.Fail(new OperationResult(OperationResultType.Failed, new EventMessages())); // TODO Remove default implmentation in a future version } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 5b13349dbd..0206442897 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,80 +1,60 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_5_0; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; /// -/// Represents the Umbraco CMS migration plan. +/// Represents the Umbraco CMS migration plan. /// -/// +/// public class UmbracoPlan : MigrationPlan { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Umbraco version. - public UmbracoPlan(IUmbracoVersion umbracoVersion) + public UmbracoPlan(IUmbracoVersion umbracoVersion) // TODO (V12): Remove unused parameter : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) - { - DefinePlan(); - } + => DefinePlan(); /// /// - /// The default initial state in plans is string.Empty. - /// - /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper - /// migrations. - /// - /// - /// This is also where we detect the current version, and reject invalid - /// upgrades (from a tool old version, or going back in time, etc). - /// + /// This is set to the final migration state of 9.4, making that the lowest supported version to upgrade from. /// public override string InitialState => "{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"; - - /// - /// Defines the plan. + /// Defines the plan. /// protected void DefinePlan() { - // MODIFYING THE PLAN - // // Please take great care when modifying the plan! // - // * Creating a migration for version 8: - // Append the migration to the main chain, using a new guid, before the "//FINAL" comment - // + // Creating a migration: append the migration to the main chain, using a new GUID. // // If the new migration causes a merge conflict, because someone else also added another // new migration, you NEED to fix the conflict by providing one default path, and paths // out of the conflict states, eg: // - // .From("state-1") - // .To("state-a") - // .To("state-b") // Some might already be in this state, without having applied ChangeA + // From("state-1") + // To("state-a") + // To("state-b") // Some might already be in this state, without having applied ChangeA // - // .From("state-1") - // .Merge() + // From("state-1") + // .Merge() // .To("state-a") - // .With() + // .With() // .To("state-b") - // .As("state-2"); + // .As("state-2"); From(InitialState); - // TO 10.0.0 - To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + // To 10.0.0 + To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); - // TO 10.2.0 - To("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}"); - To("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}"); + // To 10.2.0 + To("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}"); + To("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}"); // To 10.3.0 To("{56833770-3B7E-4FD5-A3B6-3416A26A7A3F}"); @@ -82,7 +62,10 @@ public class UmbracoPlan : MigrationPlan // To 10.4.0 To("{3F5D492A-A3DB-43F9-A73E-9FEE3B180E6C}"); - // to 10.5.0 / 11.2.0 - To("{83AF7945-DADE-4A02-9041-F3F6EBFAC319}"); + // To 10.5.0 / 11.2.0 + To("{83AF7945-DADE-4A02-9041-F3F6EBFAC319}"); + + // To 11.3.0 + To("{BB3889ED-E2DE-49F2-8F71-5FD8616A2661}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs new file mode 100644 index 0000000000..b8668de31f --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs @@ -0,0 +1,39 @@ +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_11_3_0; + +public class AddDomainSortOrder : MigrationBase +{ + public AddDomainSortOrder(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() + { + if (ColumnExists(DomainDto.TableName, "sortOrder") == false) + { + // Use a custom SQL query to prevent selecting explicit columns (sortOrder doesn't exist yet) + List domainDtos = Database.Fetch($"SELECT * FROM {DomainDto.TableName}"); + + Delete.Table(DomainDto.TableName).Do(); + Create.Table().Do(); + + foreach (DomainDto domainDto in domainDtos) + { + bool isWildcard = string.IsNullOrWhiteSpace(domainDto.DomainName) || domainDto.DomainName.StartsWith("*"); + if (isWildcard) + { + // Set sort order of wildcard domains to -1 + domainDto.SortOrder = -1; + } + else + { + // Keep exising sort order by setting it to the id + domainDto.SortOrder = domainDto.Id; + } + } + + Database.InsertBatch(domainDtos); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs index 31a04fd664..da5a8ad665 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs @@ -4,11 +4,13 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; -[TableName(Constants.DatabaseSchema.Tables.Domain)] +[TableName(TableName)] [PrimaryKey("id")] [ExplicitColumns] internal class DomainDto { + public const string TableName = Constants.DatabaseSchema.Tables.Domain; + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } @@ -26,8 +28,11 @@ internal class DomainDto public string DomainName { get; set; } = null!; /// - /// Used for a result on the query to get the associated language for a domain if there is one + /// Used for a result on the query to get the associated language for a domain, if there is one. /// [ResultColumn("languageISOCode")] public string IsoCode { get; set; } = null!; + + [Column("sortOrder")] + public int SortOrder { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DomainFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DomainFactory.cs new file mode 100644 index 0000000000..fc95c03dcd --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DomainFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DomainFactory +{ + public static IDomain BuildEntity(DomainDto dto) + { + var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) + { + Id = dto.Id, + LanguageId = dto.DefaultLanguage, + RootContentId = dto.RootStructureId, + SortOrder = dto.SortOrder, + }; + + // Reset dirty initial properties (U4-1946) + domain.ResetDirtyProperties(false); + + return domain; + } + + public static DomainDto BuildDto(IDomain entity) + { + var dto = new DomainDto + { + Id = entity.Id, + DefaultLanguage = entity.LanguageId, + RootStructureId = entity.RootContentId, + DomainName = entity.DomainName, + SortOrder = entity.SortOrder, + }; + + return dto; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs index 2f7b3991d2..1efc496a91 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs @@ -18,5 +18,6 @@ public sealed class DomainMapper : BaseMapper DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); + DefineMap(nameof(UmbracoDomain.SortOrder), nameof(DomainDto.SortOrder)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs index 9304d27b84..b1ab5f9437 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs @@ -7,40 +7,36 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -// TODO: We need to get a readonly ISO code for the domain assigned internal class DomainRepository : EntityRepositoryBase, IDomainRepository { public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { - } + { } - public IDomain? GetByName(string domainName) => - GetMany().FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); + public IDomain? GetByName(string domainName) + => GetMany().FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); - public bool Exists(string domainName) => GetMany().Any(x => x.DomainName.InvariantEquals(domainName)); + public bool Exists(string domainName) + => GetMany().Any(x => x.DomainName.InvariantEquals(domainName)); - public IEnumerable GetAll(bool includeWildcards) => - GetMany().Where(x => includeWildcards || x.IsWildcard == false); + public IEnumerable GetAll(bool includeWildcards) + => GetMany().Where(x => includeWildcards || x.IsWildcard == false); - public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) => - GetMany() - .Where(x => x.RootContentId == contentId) - .Where(x => includeWildcards || x.IsWildcard == false); + public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) + => GetMany().Where(x => x.RootContentId == contentId).Where(x => includeWildcards || x.IsWildcard == false); - protected override IRepositoryCachePolicy CreateCachePolicy() => - new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ - false); + protected override IRepositoryCachePolicy CreateCachePolicy() + => new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, false); - protected override IDomain? PerformGet(int id) => - - // use the underlying GetAll which will force cache all domains - GetMany().FirstOrDefault(x => x.Id == id); + protected override IDomain? PerformGet(int id) + // Use the underlying GetAll which will force cache all domains + => GetMany().FirstOrDefault(x => x.Id == id); protected override IEnumerable PerformGetAll(params int[]? ids) { @@ -49,12 +45,13 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep { sql.WhereIn(x => x.Id, ids); } + sql.OrderBy(dto => dto.SortOrder); - return Database.Fetch(sql).Select(ConvertFromDto); + return Database.Fetch(sql).Select(DomainFactory.BuildEntity); } - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotSupportedException("This repository does not support this method"); + protected override IEnumerable PerformGetByQuery(IQuery query) + => throw new NotSupportedException("This repository does not support this method"); protected override Sql GetBaseQuery(bool isCount) { @@ -65,7 +62,7 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep } else { - sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") + sql.Select($"{Constants.DatabaseSchema.Tables.Domain}.*, {Constants.DatabaseSchema.Tables.Language}.languageISOCode") .From() .LeftJoin() .On(dto => dto.DefaultLanguage, dto => dto.Id); @@ -74,23 +71,23 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep return sql; } - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; + protected override string GetBaseWhereClause() + => $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; protected override IEnumerable GetDeleteClauses() - { - var list = new List { "DELETE FROM umbracoDomain WHERE id = @id" }; - return list; - } + => new [] + { + $"DELETE FROM {Constants.DatabaseSchema.Tables.Domain} WHERE id = @id", + }; protected override void PersistNewItem(IDomain entity) { var exists = Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Domain} WHERE domainName = @domainName", new { domainName = entity.DomainName }); if (exists > 0) { - throw new DuplicateNameException( - string.Format("The domain name {0} is already assigned", entity.DomainName)); + throw new DuplicateNameException($"The domain name {entity.DomainName} is already assigned."); } if (entity.RootContentId.HasValue) @@ -100,34 +97,37 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep new { id = entity.RootContentId.Value }); if (contentExists == 0) { - throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + throw new NullReferenceException($"No content exists with id {entity.RootContentId.Value}."); } } if (entity.LanguageId.HasValue) { var languageExists = Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Language} WHERE id = @id", new { id = entity.LanguageId.Value }); if (languageExists == 0) { - throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); + throw new NullReferenceException($"No language exists with id {entity.LanguageId.Value}."); } } entity.AddingEntity(); - var factory = new DomainModelFactory(); - DomainDto dto = factory.BuildDto(entity); + // Get sort order + entity.SortOrder = GetNewSortOrder(entity.RootContentId, entity.IsWildcard); + + DomainDto dto = DomainFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; - // if the language changed, we need to resolve the ISO code! + // If the language changed, we need to resolve the ISO code if (entity.LanguageId.HasValue) { ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( - "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + $"SELECT languageISOCode FROM {Constants.DatabaseSchema.Tables.Language} WHERE id = @langId", + new { langId = entity.LanguageId }); } entity.ResetDirtyProperties(); @@ -137,15 +137,13 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep { entity.UpdatingEntity(); + // Ensure there is no other domain with the same name on another entity var exists = Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Domain} WHERE domainName = @domainName AND umbracoDomain.id <> @id", new { domainName = entity.DomainName, id = entity.Id }); - - // ensure there is no other domain with the same name on another entity if (exists > 0) { - throw new DuplicateNameException( - string.Format("The domain name {0} is already assigned", entity.DomainName)); + throw new DuplicateNameException($"The domain name {entity.DomainName} is already assigned."); } if (entity.RootContentId.HasValue) @@ -155,69 +153,40 @@ internal class DomainRepository : EntityRepositoryBase, IDomainRep new { id = entity.RootContentId.Value }); if (contentExists == 0) { - throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + throw new NullReferenceException($"No content exists with id {entity.RootContentId.Value}."); } } if (entity.LanguageId.HasValue) { var languageExists = Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Language} WHERE id = @id", new { id = entity.LanguageId.Value }); if (languageExists == 0) { - throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); + throw new NullReferenceException($"No language exists with id {entity.LanguageId.Value}."); } } - var factory = new DomainModelFactory(); - DomainDto dto = factory.BuildDto(entity); + DomainDto dto = DomainFactory.BuildDto(entity); Database.Update(dto); - // if the language changed, we need to resolve the ISO code! + // If the language changed, we need to resolve the ISO code if (entity.WasPropertyDirty("LanguageId")) { ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( - "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + $"SELECT languageISOCode FROM {Constants.DatabaseSchema.Tables.Language} WHERE id = @langId", + new { langId = entity.LanguageId }); } entity.ResetDirtyProperties(); } - private IDomain ConvertFromDto(DomainDto dto) - { - var factory = new DomainModelFactory(); - IDomain entity = factory.BuildEntity(dto); - return entity; - } - - internal class DomainModelFactory - { - public IDomain BuildEntity(DomainDto dto) - { - var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) - { - Id = dto.Id, - LanguageId = dto.DefaultLanguage, - RootContentId = dto.RootStructureId, - }; - - // reset dirty initial properties (U4-1946) - domain.ResetDirtyProperties(false); - return domain; - } - - public DomainDto BuildDto(IDomain entity) - { - var dto = new DomainDto - { - DefaultLanguage = entity.LanguageId, - DomainName = entity.DomainName, - Id = entity.Id, - RootStructureId = entity.RootContentId, - }; - return dto; - } - } + protected int GetNewSortOrder(int? rootContentId, bool isWildcard) + => isWildcard + ? -1 + : Database.ExecuteScalar( + $"SELECT COALESCE(MAX(sortOrder), -1) + 1 FROM {Constants.DatabaseSchema.Tables.Domain} WHERE domainRootStructureID = @rootContentId AND NOT (domainName = '' OR domainName LIKE '*%')", + new { rootContentId }); } diff --git a/src/Umbraco.PublishedCache.NuCache/DomainCache.cs b/src/Umbraco.PublishedCache.NuCache/DomainCache.cs index c4b0162833..27d9cd35c8 100644 --- a/src/Umbraco.PublishedCache.NuCache/DomainCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/DomainCache.cs @@ -31,7 +31,7 @@ public class DomainCache : IDomainCache list = list.Where(x => x.IsWildcard == false); } - return list; + return list.OrderBy(x => x.SortOrder); } /// @@ -46,7 +46,7 @@ public class DomainCache : IDomainCache list = list.Where(x => x.IsWildcard == false); } - return list; + return list.OrderBy(x => x.SortOrder); } /// diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 42e17b603a..fe3c2836c5 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -298,15 +298,15 @@ internal class PublishedSnapshotService : IPublishedSnapshotService continue; // anomaly } - if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) + var culture = domain.LanguageIsoCode; + if (string.IsNullOrWhiteSpace(culture)) { continue; // anomaly } - var culture = domain.LanguageIsoCode; _domainStore.SetLocked( domain.Id, - new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); + new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder)); break; } } @@ -832,7 +832,7 @@ internal class PublishedSnapshotService : IPublishedSnapshotService { foreach (Domain domain in domains .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) - .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard))) + .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder))) { _domainStore.SetLocked(domain.Id, domain); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 99a4ea262d..642db289a0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2246,16 +2246,14 @@ public class ContentController : ContentControllerBase public ContentDomainsAndCulture GetCultureAndDomains(int id) { - IDomain[]? nodeDomains = _domainService.GetAssignedDomains(id, true)?.ToArray(); - IDomain? wildcard = nodeDomains?.FirstOrDefault(d => d.IsWildcard); - IEnumerable? domains = nodeDomains?.Where(d => !d.IsWildcard) - .Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0))); + IDomain[] assignedDomains = _domainService.GetAssignedDomains(id, true).ToArray(); + IDomain? wildcard = assignedDomains.FirstOrDefault(d => d.IsWildcard); + IEnumerable domains = assignedDomains.Where(d => !d.IsWildcard).Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0))); + return new ContentDomainsAndCulture { + Language = wildcard == null || !wildcard.LanguageId.HasValue ? "undefined" : wildcard.LanguageId.ToString(), Domains = domains, - Language = wildcard == null || !wildcard.LanguageId.HasValue - ? "undefined" - : wildcard.LanguageId.ToString() }; } @@ -2264,11 +2262,11 @@ public class ContentController : ContentControllerBase { if (model.Domains is not null) { - foreach (DomainDisplay domain in model.Domains) + foreach (DomainDisplay domainDisplay in model.Domains) { try { - Uri uri = DomainUtilities.ParseUriFromDomainName(domain.Name, new Uri(Request.GetEncodedUrl())); + DomainUtilities.ParseUriFromDomainName(domainDisplay.Name, new Uri(Request.GetEncodedUrl())); } catch (UriFormatException) { @@ -2277,18 +2275,16 @@ public class ContentController : ContentControllerBase } } + // Validate node IContent? node = _contentService.GetById(model.NodeId); - if (node == null) { HttpContext.SetReasonPhrase("Node Not Found."); return NotFound("There is no content node with id {model.NodeId}."); } - EntityPermission? permission = - _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path); - - + // Validate permissions on node + EntityPermission? permission = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path); if (permission?.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) { HttpContext.SetReasonPhrase("Permission Denied."); @@ -2296,120 +2292,118 @@ public class ContentController : ContentControllerBase } model.Valid = true; - IDomain[]? domains = _domainService.GetAssignedDomains(model.NodeId, true)?.ToArray(); - ILanguage[] languages = _localizationService.GetAllLanguages().ToArray(); - ILanguage? language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; - // process wildcard - if (language != null) + IDomain[] assignedDomains = _domainService.GetAssignedDomains(model.NodeId, true).ToArray(); + ILanguage[] languages = _localizationService.GetAllLanguages().ToArray(); + + // Process language + ILanguage? language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; + if (language is not null) { - // yet there is a race condition here... - IDomain? wildcard = domains?.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) + // Update or create language on wildcard domain + IDomain? assignedWildcardDomain = assignedDomains.FirstOrDefault(d => d.IsWildcard); + if (assignedWildcardDomain is not null) { - wildcard.LanguageId = language.Id; + assignedWildcardDomain.LanguageId = language.Id; } else { - wildcard = new UmbracoDomain("*" + model.NodeId) + assignedWildcardDomain = new UmbracoDomain("*" + model.NodeId) { LanguageId = model.Language, RootContentId = model.NodeId }; } - Attempt saveAttempt = _domainService.Save(wildcard); - if (saveAttempt == false) + Attempt saveAttempt = _domainService.Save(assignedWildcardDomain); + if (saveAttempt.Success == false) { HttpContext.SetReasonPhrase(saveAttempt.Result?.Result.ToString()); return BadRequest("Saving domain failed"); } } - else + + // Delete every domain that's in the database, but not in the model + foreach (IDomain? assignedDomain in assignedDomains.Where(d => (d.IsWildcard && language is null) || (d.IsWildcard == false && (model.Domains is null || model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))))) { - IDomain? wildcard = domains?.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) - { - _domainService.Delete(wildcard); - } + _domainService.Delete(assignedDomain); } - // process domains - // delete every (non-wildcard) domain, that exists in the DB yet is not in the model - foreach (IDomain domain in domains?.Where(d => - d.IsWildcard == false && - (model.Domains?.All(m => m.Name.InvariantEquals(d.DomainName) == false) ?? - false)) ?? - Array.Empty()) + // Process domains + if (model.Domains is not null) { - _domainService.Delete(domain); - } - - var names = new List(); - - // create or update domains in the model - foreach (DomainDisplay domainModel in model.Domains?.Where(m => string.IsNullOrWhiteSpace(m.Name) == false) ?? - Array.Empty()) - { - language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); - if (language == null) + var savedDomains = new List(); + foreach (DomainDisplay domainDisplay in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) { - continue; - } - - var name = domainModel.Name.ToLowerInvariant(); - if (names.Contains(name)) - { - domainModel.Duplicate = true; - continue; - } - - names.Add(name); - IDomain? domain = domains?.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); - if (domain != null) - { - domain.LanguageId = language.Id; - _domainService.Save(domain); - } - else if (_domainService.Exists(domainModel.Name)) - { - domainModel.Duplicate = true; - IDomain? xdomain = _domainService.GetByName(domainModel.Name); - var xrcid = xdomain?.RootContentId; - if (xrcid.HasValue) + language = languages.FirstOrDefault(l => l.Id == domainDisplay.Lang); + if (language == null) { - IContent? xcontent = _contentService.GetById(xrcid.Value); - var xnames = new List(); - while (xcontent != null) + continue; + } + + var domainName = domainDisplay.Name.ToLowerInvariant(); + if (savedDomains.Any(d => d.DomainName == domainName)) + { + domainDisplay.Duplicate = true; + continue; + } + + IDomain? domain = assignedDomains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainName)); + if (domain is null && _domainService.GetByName(domainName) is IDomain existingDomain) + { + // Domain name already exists on another node + domainDisplay.Duplicate = true; + + // Add node breadcrumbs + if (existingDomain.RootContentId is int rootContentId) { - if (xcontent.Name is not null) + var breadcrumbs = new List(); + + IContent? content = _contentService.GetById(rootContentId); + while (content is not null) { - xnames.Add(xcontent.Name); + breadcrumbs.Add(content.Name); + if (content.ParentId < -1) + { + breadcrumbs.Add("Recycle Bin"); + } + + content = _contentService.GetParent(content); } - if (xcontent.ParentId < -1) - { - xnames.Add("Recycle Bin"); - } - - xcontent = _contentService.GetParent(xcontent); + breadcrumbs.Reverse(); + domainDisplay.Other = "/" + string.Join("/", breadcrumbs); } - xnames.Reverse(); - domainModel.Other = "/" + string.Join("/", xnames); + continue; } - } - else - { - // yet there is a race condition here... - var newDomain = new UmbracoDomain(name) { LanguageId = domainModel.Lang, RootContentId = model.NodeId }; - Attempt saveAttempt = _domainService.Save(newDomain); - if (saveAttempt == false) + + // Update or create domain + if (domain != null) + { + domain.LanguageId = language.Id; + } + else + { + domain = new UmbracoDomain(domainName) + { + LanguageId = language.Id, + RootContentId = model.NodeId, + }; + } + + Attempt saveAttempt = _domainService.Save(domain); + if (saveAttempt.Success == false) { HttpContext.SetReasonPhrase(saveAttempt.Result?.Result.ToString()); - return BadRequest("Saving new domain failed"); + return BadRequest("Saving domain failed"); } + + savedDomains.Add(domain); } + + // Sort saved domains + _domainService.Sort(savedDomains); } model.Valid = model.Domains?.All(m => m.Duplicate == false) ?? false; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js index 989c051e03..d9c18fac01 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js @@ -88,56 +88,83 @@ Use this directive to render a button with a dropdown of alternative actions. @param {string=} direction Set the direction of the dropdown ("up", "down"). @param {string=} float Set the float of the dropdown. ("left", "right"). **/ - (function () { - 'use strict'; + 'use strict'; - function ButtonGroupDirective() { + function ButtonGroupDirective() { - function link(scope) { + function controller($scope) { + $scope.toggleStyle = null; + $scope.blockElement = false; - scope.dropdown = { - isOpen: false - }; - - scope.toggleDropdown = function() { - scope.dropdown.isOpen = !scope.dropdown.isOpen; - }; - - scope.closeDropdown = function() { - scope.dropdown.isOpen = false; - }; - - scope.executeMenuItem = function(subButton) { - subButton.handler(); - scope.closeDropdown(); - }; + var buttonStyle = $scope.buttonStyle; + if (buttonStyle) { + // Make it possible to pass in multiple styles + if (buttonStyle.startsWith("[") && buttonStyle.endsWith("]")) { + // when using an attr it will always be a string so we need to remove square brackets and turn it into and array + var withoutBrackets = buttonStyle.replace(/[\[\]']+/g, ''); + // split array by , + make sure to catch whitespaces + var array = withoutBrackets.split(/\s?,\s?/g); + Utilities.forEach(array, item => { + if (item === "block") { + $scope.blockElement = true; + } else { + $scope.toggleStyle = ($scope.toggleStyle ? $scope.toggleStyle + " " : "") + "btn-" + item; + } + }); + } else { + if (buttonStyle === "block") { + $scope.blockElement = true; + } else { + $scope.toggleStyle = "btn-" + buttonStyle; + } } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/buttons/umb-button-group.html', - scope: { - defaultButton: "=", - subButtons: "=", - state: "=?", - direction: "@?", - float: "@?", - buttonStyle: "@?", - size: "@?", - icon: "@?", - label: "@?", - labelKey: "@?", - disabled: " .btn-large:last-child { .border-radius(0 0 @borderRadiusLarge @borderRadiusLarge); } + +.btn-group-justified { + display: flex; + + .umb-button { + margin-left: 0; + } + + > * { + flex-grow: 1; + } + + > .dropdown-toggle { + flex-grow: 0; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index e0916a070f..7faf746022 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -577,11 +577,6 @@ div.help { margin-top: 5px; } - -table.domains .help-inline { - color:@red; -} - // INPUT GROUPS // ------------ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html index 2a4a43769d..0462482112 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -1,4 +1,4 @@ -
+
+ - - -
- -
- - -
Culture
- - -
- - - -
-
-
{{vm.error.errorMsg}}
-
{{vm.error.data.Message}}
-
-
- -
Domains
- - Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". - Furthermore also one-level paths in domains are supported, eg. "example.com/en" or "/en". - -
- - - - - - - - - - - - - - - -
- Domain - * - - Language - * -
- - - - - Value cannot be empty - - Domain has already been assigned.({{domain.other}}) - - - - - -
-
- - - - - - - -
+ +
+ +
Culture
+ + +
+ +
+
+
{{vm.error.errorMsg}}
+
{{vm.error.data.Message}}
+
- - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js index 2cec5fb17a..61d710427e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js @@ -1,166 +1,178 @@ (function () { - "use strict"; + "use strict"; - function AssignDomainController($scope, localizationService, languageResource, contentResource, navigationService, notificationsService, $location) { - var vm = this; - - vm.closeDialog = closeDialog; - vm.addDomain = addDomain; - vm.addCurrentDomain = addCurrentDomain; - vm.removeDomain = removeDomain; - vm.save = save; - vm.languages = []; - vm.domains = []; - vm.language = null; + function AssignDomainController($scope, localizationService, languageResource, contentResource, navigationService, notificationsService, $location) { + var vm = this; - function activate() { + vm.loading = true; + vm.closeDialog = closeDialog; + vm.removeDomain = removeDomain; + vm.save = save; + vm.languages = []; + vm.domains = []; + vm.language = null; - vm.loading = true; + vm.buttonGroup = { + defaultButton: { + labelKey: 'assignDomain_addNew', + buttonStyle: 'info', + handler: addDomain + }, + subButtons: [{ + labelKey: 'assignDomain_addCurrent', + buttonStyle: 'success', + handler: addCurrentDomain + }] + }; - languageResource.getAll().then(langs => { - vm.languages = langs; + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + handle: ".handle", + placeholder: 'sortable-placeholder', + forcePlaceholderSize: true, + tolerance: 'pointer' + }; - var defLang = langs.filter(l => { - return l.isDefault; - }); + function activate() { + languageResource.getAll().then(langs => { + vm.languages = langs; - if (defLang.length > 0) { - vm.defaultLanguage = defLang[0]; - } - else { - vm.defaultLanguage = langs[0]; - } - getCultureAndDomains().then(() => { - vm.loading = false; - }); + var defLang = langs.filter(l => { + return l.isDefault; + }); + + if (defLang.length > 0) { + vm.defaultLanguage = defLang[0]; + } + else { + vm.defaultLanguage = langs[0]; + } + + contentResource.getCultureAndDomains($scope.currentNode.id).then(function (data) { + if (data.language !== "undefined") { + var lang = vm.languages.filter(function (l) { + return matchLanguageById(l, data.language); }); - localizationService.localize("assignDomain_inherit").then(function (value) { - vm.inherit = value; - }); - - } - - function getCultureAndDomains () { - return contentResource.getCultureAndDomains($scope.currentNode.id) - .then(function (data) { - - if (data.language !== "undefined") { - var lang = vm.languages.filter(function (l) { - return matchLanguageById(l, data.language); - }); - if (lang.length > 0) { - vm.language = lang[0]; - } - } - - vm.domains = data.domains.map(function (d) { - var matchedLangs = vm.languages.filter(function (lng) { - return matchLanguageById(lng, d.lang); - }); - return { - name: d.name, - lang: matchedLangs.length > 0 ? matchedLangs[0] : vm.defaultLanguage - } - }); - }); - } - - function matchLanguageById(language, id) { - var langId = parseInt(language.id); - var comparisonId = parseInt(id); - return langId === comparisonId; - } - - function closeDialog() { - navigationService.hideDialog(); - } - - function addDomain() { - vm.domains.push({ - name: '', - lang: vm.defaultLanguage - }); - } - - function addCurrentDomain() { - var domainToAdd = $location.host(); - var port = $location.port(); - if (port != 80 && port != 443) { - domainToAdd += ":" + port; + if (lang.length > 0) { + vm.language = lang[0]; + } } - vm.domains.push({ - name: domainToAdd, - lang: vm.defaultLanguage + + vm.domains = data.domains.map(function (d) { + var matchedLangs = vm.languages.filter(function (lng) { + return matchLanguageById(lng, d.lang); + }); + + return { + name: d.name, + lang: matchedLangs.length > 0 ? matchedLangs[0] : vm.defaultLanguage + } }); - } - function removeDomain(index) { - vm.domains.splice(index, 1); - } - - function save() { - - vm.error = null; - vm.submitButtonState = "busy"; - - if (vm.domainForm.$valid) { - - // clear validation messages - vm.domains.forEach(domain => { - domain.duplicate = null; - domain.other = null; - }); - - var data = { - nodeId: $scope.currentNode.id, - domains: vm.domains.map(function (d) { - return { - name: d.name, - lang: d.lang.id - }; - }), - language: vm.language != null ? vm.language.id : 0 - }; - - contentResource.saveLanguageAndDomains(data).then(function (response) { - - // validation is interesting. Check if response is valid - if(response.valid) { - vm.submitButtonState = "success"; - localizationService.localize('speechBubbles_editCulturesAndHostnamesSaved').then(function(value) { - notificationsService.success(value); - }); - closeDialog(); - - // show validation messages for each domain - } else { - response.domains.forEach(validation => { - vm.domains.forEach(domain => { - if(validation.name === domain.name) { - domain.duplicate = validation.duplicate; - domain.other = validation.other; - } - }); - }); - vm.submitButtonState = "error"; - localizationService.localize('speechBubbles_editCulturesAndHostnamesError').then(function(value) { - notificationsService.error(value); - }); - } - - }, function (e) { - vm.error = e; - vm.submitButtonState = "error"; - }); - } - else { - vm.submitButtonState = "error"; - } - } - - activate(); + vm.loading = false; + }); + }); } - angular.module("umbraco").controller("Umbraco.Editors.Content.AssignDomainController", AssignDomainController); + + function matchLanguageById(language, id) { + var langId = parseInt(language.id); + var comparisonId = parseInt(id); + + return langId === comparisonId; + } + + function closeDialog() { + navigationService.hideDialog(); + } + + function addDomain() { + vm.domains.push({ + name: '', + lang: vm.defaultLanguage + }); + } + + function addCurrentDomain() { + var domainToAdd = $location.host(); + var port = $location.port(); + if (port != 80 && port != 443) { + domainToAdd += ":" + port; + } + + vm.domains.push({ + name: domainToAdd, + lang: vm.defaultLanguage + }); + } + + function removeDomain(index) { + vm.domains.splice(index, 1); + } + + function save() { + vm.error = null; + vm.submitButtonState = "busy"; + + if (vm.domainForm.$valid) { + // clear validation messages + vm.domains.forEach(domain => { + domain.duplicate = null; + domain.other = null; + }); + + var data = { + nodeId: $scope.currentNode.id, + domains: vm.domains.map(function (d) { + return { + name: d.name, + lang: d.lang.id + }; + }), + language: vm.language != null ? vm.language.id : 0 + }; + + contentResource.saveLanguageAndDomains(data).then(function (response) { + // validation is interesting. Check if response is valid + if (response.valid) { + vm.submitButtonState = "success"; + + localizationService.localize('speechBubbles_editCulturesAndHostnamesSaved').then(function (value) { + notificationsService.success(value); + }); + + closeDialog(); + } else { + // show validation messages for each domain + response.domains.forEach(validation => { + vm.domains.forEach(domain => { + if (validation.name === domain.name) { + domain.duplicate = validation.duplicate; + domain.other = validation.other; + } + }); + }); + + vm.submitButtonState = "error"; + + localizationService.localize('speechBubbles_editCulturesAndHostnamesError').then(function (value) { + notificationsService.error(value); + }); + } + }, function (e) { + vm.error = e; + vm.submitButtonState = "error"; + }); + } else { + vm.submitButtonState = "error"; + } + } + + activate(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.AssignDomainController", AssignDomainController); })(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs index e877ace137..f0417b663e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlTests.cs @@ -129,7 +129,7 @@ public class ContentFinderByUrlTests : PublishedSnapshotServiceTestBase var (finder, frequest) = await GetContentFinder(urlString); - frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite", -1, "en-US", false), new Uri("http://mysite/"))); + frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite", -1, "en-US", false, 0), new Uri("http://mysite/"))); var result = await finder.TryFindContent(frequest); @@ -155,7 +155,7 @@ public class ContentFinderByUrlTests : PublishedSnapshotServiceTestBase var (finder, frequest) = await GetContentFinder(urlString); - frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite/æøå", -1, "en-US", false), new Uri("http://mysite/æøå"))); + frequest.SetDomain(new DomainAndUri(new Domain(1, "mysite/æøå", -1, "en-US", false, 0), new Uri("http://mysite/æøå"))); var result = await finder.TryFindContent(frequest); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/SiteDomainMapperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/SiteDomainMapperTests.cs index 35bc5c4b62..a0361f0222 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/SiteDomainMapperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/SiteDomainMapperTests.cs @@ -192,8 +192,8 @@ public class SiteDomainMapperTests var current = new Uri("https://domain1.com/foo/bar"); Domain[] domains = { - new Domain(1, "domain2.com", -1, s_cultureFr, false), - new Domain(1, "domain1.com", -1, s_cultureGb, false), + new Domain(1, "domain2.com", -1, s_cultureFr, false, 0), + new Domain(1, "domain1.com", -1, s_cultureGb, false, 1), }; var domainAndUris = DomainAndUris(current, domains); var output = siteDomainMapper.MapDomain(domainAndUris, current, s_cultureFr, s_cultureFr).Uri.ToString(); @@ -203,8 +203,8 @@ public class SiteDomainMapperTests current = new Uri("https://domain1.com/foo/bar"); domains = new[] { - new Domain(1, "https://domain1.com", -1, s_cultureFr, false), - new Domain(1, "https://domain2.com", -1, s_cultureGb, false), + new Domain(1, "https://domain1.com", -1, s_cultureFr, false, 0), + new Domain(1, "https://domain2.com", -1, s_cultureGb, false, 1), }; domainAndUris = DomainAndUris(current, domains); output = siteDomainMapper.MapDomain(domainAndUris, current, s_cultureFr, s_cultureFr).Uri.ToString(); @@ -213,8 +213,8 @@ public class SiteDomainMapperTests current = new Uri("https://domain1.com/foo/bar"); domains = new[] { - new Domain(1, "https://domain1.com", -1, s_cultureFr, false), - new Domain(1, "https://domain4.com", -1, s_cultureGb, false), + new Domain(1, "https://domain1.com", -1, s_cultureFr, false, 0), + new Domain(1, "https://domain4.com", -1, s_cultureGb, false, 1), }; domainAndUris = DomainAndUris(current, domains); output = siteDomainMapper.MapDomain(domainAndUris, current, s_cultureFr, s_cultureFr).Uri.ToString(); @@ -223,8 +223,8 @@ public class SiteDomainMapperTests current = new Uri("https://domain4.com/foo/bar"); domains = new[] { - new Domain(1, "https://domain1.com", -1, s_cultureFr, false), - new Domain(1, "https://domain4.com", -1, s_cultureGb, false), + new Domain(1, "https://domain1.com", -1, s_cultureFr, false, 0), + new Domain(1, "https://domain4.com", -1, s_cultureGb, false, 1), }; domainAndUris = DomainAndUris(current, domains); output = siteDomainMapper.MapDomain(domainAndUris, current, s_cultureFr, s_cultureFr).Uri.ToString(); @@ -247,8 +247,8 @@ public class SiteDomainMapperTests var output = siteDomainMapper.MapDomain( new[] { - new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false), current), - new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false), current), + new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false, 0), current), + new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false, 1), current), }, current, s_cultureFr, @@ -261,8 +261,8 @@ public class SiteDomainMapperTests output = siteDomainMapper.MapDomain( new[] { - new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false), current), - new DomainAndUri(new Domain(1, "domain2.net", -1, s_cultureGb, false), current), + new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false, 0), current), + new DomainAndUri(new Domain(1, "domain2.net", -1, s_cultureGb, false, 1), current), }, current, s_cultureFr, @@ -276,8 +276,8 @@ public class SiteDomainMapperTests output = siteDomainMapper.MapDomain( new[] { - new DomainAndUri(new Domain(1, "domain2.net", -1, s_cultureFr, false), current), - new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureGb, false), current), + new DomainAndUri(new Domain(1, "domain2.net", -1, s_cultureFr, false, 0), current), + new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureGb, false, 1), current), }, current, s_cultureFr, @@ -305,38 +305,38 @@ public class SiteDomainMapperTests var output = siteDomainMapper.MapDomains( new[] { - new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false), current), // no: current + what MapDomain would pick - new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false), current), // yes: same site (though bogus setup) + new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false, 0), current), // no: current + what MapDomain would pick + new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false, 1), current), // no: not same site + new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false, 2), current), // no: not same site + new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false, 3), current), // no: not same site + new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false, 4), current), // yes: same site (though bogus setup) }, current, true, s_cultureFr, s_cultureFr).ToArray(); - Assert.AreEqual(1, output.Count()); - Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.AreEqual(1, output.Length); + Assert.AreEqual("http://domain1.org/", output[0].Uri.ToString()); // current is a site1 uri, domains does not contain current current = new Uri("http://domain1.com/foo/bar"); output = siteDomainMapper.MapDomains( new[] { - new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false), current), // no: what MapDomain would pick - new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false), current), // yes: same site (though bogus setup) + new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false, 0), current), // no: what MapDomain would pick + new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false, 1), current), // no: not same site + new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false, 2), current), // no: not same site + new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false, 3), current), // no: not same site + new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false, 4), current), // yes: same site (though bogus setup) }, current, true, s_cultureFr, s_cultureFr).ToArray(); - Assert.AreEqual(1, output.Count()); - Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.AreEqual(1, output.Length); + Assert.AreEqual("http://domain1.org/", output[0].Uri.ToString()); siteDomainMapper.BindSites("site1", "site3"); siteDomainMapper.BindSites("site2", "site4"); @@ -346,43 +346,43 @@ public class SiteDomainMapperTests output = siteDomainMapper.MapDomains( new[] { - new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false), current), // no: current + what MapDomain would pick - new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false), current), // yes: bound site - new DomainAndUri(new Domain(1, "domain3.org", -1, s_cultureGb, false), current), // yes: bound site - new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false), current), // yes: same site (though bogus setup) + new DomainAndUri(new Domain(1, "domain1.com", -1, s_cultureFr, false, 0), current), // no: current + what MapDomain would pick + new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false, 1), current), // no: not same site + new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false, 2), current), // yes: bound site + new DomainAndUri(new Domain(1, "domain3.org", -1, s_cultureGb, false, 3), current), // yes: bound site + new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false, 4), current), // no: not same site + new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false, 5), current), // yes: same site (though bogus setup) }, current, true, s_cultureFr, s_cultureFr).ToArray(); - Assert.AreEqual(3, output.Count()); - Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); - Assert.Contains("http://domain3.com/", output.Select(d => d.Uri.ToString()).ToArray()); - Assert.Contains("http://domain3.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.AreEqual(3, output.Length); + Assert.AreEqual("http://domain3.com/", output[0].Uri.ToString()); + Assert.AreEqual("http://domain3.org/", output[1].Uri.ToString()); + Assert.AreEqual("http://domain1.org/", output[2].Uri.ToString()); // current is a site1 uri, domains does not contain current current = new Uri("http://domain1.com/foo/bar"); output = siteDomainMapper.MapDomains( new[] { - new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false), current), // no: what MapDomain would pick - new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false), current), // yes: bound site - new DomainAndUri(new Domain(1, "domain3.org", -1, s_cultureGb, false), current), // yes: bound site - new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false), current), // no: not same site - new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false), current), // yes: same site (though bogus setup) + new DomainAndUri(new Domain(1, "domain1.net", -1, s_cultureFr, false, 0), current), // no: what MapDomain would pick + new DomainAndUri(new Domain(1, "domain2.com", -1, s_cultureGb, false, 1), current), // no: not same site + new DomainAndUri(new Domain(1, "domain3.com", -1, s_cultureGb, false, 2), current), // yes: bound site + new DomainAndUri(new Domain(1, "domain3.org", -1, s_cultureGb, false, 3), current), // yes: bound site + new DomainAndUri(new Domain(1, "domain4.com", -1, s_cultureGb, false, 4), current), // no: not same site + new DomainAndUri(new Domain(1, "domain1.org", -1, s_cultureGb, false, 5), current), // yes: same site (though bogus setup) }, current, true, s_cultureFr, s_cultureFr).ToArray(); - Assert.AreEqual(3, output.Count()); - Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); - Assert.Contains("http://domain3.com/", output.Select(d => d.Uri.ToString()).ToArray()); - Assert.Contains("http://domain3.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.AreEqual(3, output.Length); + Assert.AreEqual("http://domain3.com/", output[0].Uri.ToString()); + Assert.AreEqual("http://domain3.org/", output[1].Uri.ToString()); + Assert.AreEqual("http://domain1.org/", output[2].Uri.ToString()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs index eecbd38963..dffbe656d2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsProviderWithDomainsTests.cs @@ -24,7 +24,7 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase { new UmbracoDomain("domain1.com") { - Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", + Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, }, }); } @@ -38,7 +38,7 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase { new UmbracoDomain("http://domain1.com/foo") { - Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", + Id = 1, LanguageId = LangFrId, RootContentId = 1001, LanguageIsoCode = "fr-FR", SortOrder = 0, }, }); } @@ -52,7 +52,7 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase { new UmbracoDomain("http://domain1.com/") { - Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR", + Id = 1, LanguageId = LangFrId, RootContentId = 10011, LanguageIsoCode = "fr-FR", SortOrder = 0, }, }); } @@ -66,27 +66,27 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase { new UmbracoDomain("http://domain1.com/") { - Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", + Id = 1, LanguageId = LangEngId, RootContentId = 1001, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain1.com/en") { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", + Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain1.com/fr") { - Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", + Id = 3, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, }, new UmbracoDomain("http://domain3.com/") { - Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", + Id = 4, LanguageId = LangEngId, RootContentId = 1003, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain3.com/en") { - Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", + Id = 5, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain3.com/fr") { - Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", + Id = 6, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, }, }); } @@ -100,35 +100,35 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase { new UmbracoDomain("http://domain1.com/en") { - Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", + Id = 1, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain1a.com/en") { - Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", + Id = 2, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 1, }, new UmbracoDomain("http://domain1b.com/en") { - Id = 3, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", + Id = 3, LanguageId = LangEngId, RootContentId = 10011, LanguageIsoCode = "en-US", SortOrder = 2, }, new UmbracoDomain("http://domain1.com/fr") { - Id = 4, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", + Id = 4, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 0, }, new UmbracoDomain("http://domain1a.com/fr") { - Id = 5, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", + Id = 5, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 1, }, new UmbracoDomain("http://domain1b.com/fr") { - Id = 6, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", + Id = 6, LanguageId = LangFrId, RootContentId = 10012, LanguageIsoCode = "fr-FR", SortOrder = 2, }, new UmbracoDomain("http://domain3.com/en") { - Id = 7, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", + Id = 7, LanguageId = LangEngId, RootContentId = 10031, LanguageIsoCode = "en-US", SortOrder = 0, }, new UmbracoDomain("http://domain3.com/fr") { - Id = 8, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", + Id = 8, LanguageId = LangFrId, RootContentId = 10032, LanguageIsoCode = "fr-FR", SortOrder = 0, }, }); } @@ -478,7 +478,7 @@ public class UrlsProviderWithDomainsTests : UrlRoutingTestBase } Assert.AreEqual(2, result.Length); - Assert.AreEqual(result[0].Text, "http://domain1b.com/en/1001-1-1/"); - Assert.AreEqual(result[1].Text, "http://domain1a.com/en/1001-1-1/"); + Assert.AreEqual(result[0].Text, "http://domain1a.com/en/1001-1-1/"); + Assert.AreEqual(result[1].Text, "http://domain1b.com/en/1001-1-1/"); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Web/Routing/PublishedRequestBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Web/Routing/PublishedRequestBuilderTests.cs index 4074f459da..0246d3e38b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Web/Routing/PublishedRequestBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Web/Routing/PublishedRequestBuilderTests.cs @@ -47,7 +47,7 @@ public class PublishedRequestBuilderTests sut.SetDomain( new DomainAndUri( - new Domain(1, "test", 2, "en-AU", false), new Uri("https://example.com/en-au"))); + new Domain(1, "test", 2, "en-AU", false, 0), new Uri("https://example.com/en-au"))); Assert.IsNotNull(sut.Domain); Assert.IsNotNull(sut.Culture); @@ -64,7 +64,7 @@ public class PublishedRequestBuilderTests var auCulture = "en-AU"; var usCulture = "en-US"; var domain = new DomainAndUri( - new Domain(1, "test", 2, auCulture, false), new Uri("https://example.com/en-au")); + new Domain(1, "test", 2, auCulture, false, 0), new Uri("https://example.com/en-au")); IReadOnlyDictionary headers = new Dictionary { ["Hello"] = "world" }; var redirect = "https://test.com"; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs index 342e08acbe..37a5f08286 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedContentLanguageVariantTests.cs @@ -242,6 +242,15 @@ public class PublishedContentLanguageVariantTests : PublishedSnapshotServiceTest Assert.IsNull(value); } + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_DefaultLanguage_With_Fallback() + { + var snapshot = GetPublishedSnapshot(); + var content = snapshot.Content.GetAtRoot().First(); + var value = content.Value(PublishedValueFallback, "welcomeText", "fr", fallback: Fallback.ToDefaultLanguage); + Assert.AreEqual("Welcome", value); + } + [Test] public void Do_Not_Get_Content_Recursively_Unless_Requested() {