Merge remote-tracking branch 'origin/v11/dev' into v11/dev

This commit is contained in:
Bjarke Berg
2023-02-15 13:58:22 +01:00
28 changed files with 963 additions and 701 deletions

View File

@@ -2,18 +2,58 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents a domain name, optionally assigned to a content and/or language ID.
/// </summary>
/// <seealso cref="Umbraco.Cms.Core.Models.Entities.IEntity" />
/// <seealso cref="Umbraco.Cms.Core.Models.Entities.IRememberBeingDirty" />
public interface IDomain : IEntity, IRememberBeingDirty
{
int? LanguageId { get; set; }
/// <summary>
/// Gets or sets the name of the domain.
/// </summary>
/// <value>
/// The name of the domain.
/// </value>
string DomainName { get; set; }
int? RootContentId { get; set; }
/// <summary>
/// Gets a value indicating whether this is a wildcard domain (only specifying the language of a content node).
/// </summary>
/// <value>
/// <c>true</c> if this is a wildcard domain; otherwise, <c>false</c>.
/// </value>
bool IsWildcard { get; }
/// <summary>
/// Readonly value of the language ISO code for the domain
/// Gets or sets the language ID assigned to the domain.
/// </summary>
/// <value>
/// The language ID assigned to the domain.
/// </value>
int? LanguageId { get; set; }
/// <summary>
/// Gets the language ISO code.
/// </summary>
/// <value>
/// The language ISO code.
/// </value>
string? LanguageIsoCode { get; }
/// <summary>
/// Gets or sets the root content ID assigned to the domain.
/// </summary>
/// <value>
/// The root content ID assigned to the domain.
/// </value>
int? RootContentId { get; set; }
/// <summary>
/// Gets or sets the sort order.
/// </summary>
/// <value>
/// The sort order.
/// </value>
int SortOrder { get => IsWildcard ? -1 : 0; set { } } // TODO Remove default implementation in a future version
}

View File

@@ -17,16 +17,20 @@ public struct Fallback : IEnumerable<int>
/// <summary>
/// Initializes a new instance of the <see cref="Fallback" /> struct with values.
/// </summary>
/// <param name="values">The values.</param>
private Fallback(int[] values) => _values = values;
/// <summary>
/// Gets an ordered set of fallback policies.
/// </summary>
/// <param name="values"></param>
/// <param name="values">The values.</param>
/// <returns>
/// The fallback policy.
/// </returns>
public static Fallback To(params int[] values) => new(values);
/// <summary>
/// Fallback to default value.
/// Fallback to the default value.
/// </summary>
public const int DefaultValue = 1;
@@ -41,18 +45,40 @@ public struct Fallback : IEnumerable<int>
public const int Ancestors = 3;
/// <summary>
/// Gets the fallback to default value policy.
/// Fallback to the default language.
/// </summary>
public const int DefaultLanguage = 4;
/// <summary>
/// Gets the fallback to the default language policy.
/// </summary>
/// <value>
/// The default language fallback policy.
/// </value>
public static Fallback ToDefaultLanguage => new Fallback(new[] { DefaultLanguage });
/// <summary>
/// Gets the fallback to the default value policy.
/// </summary>
/// <value>
/// The default value fallback policy.
/// </value>
public static Fallback ToDefaultValue => new(new[] { DefaultValue });
/// <summary>
/// Gets the fallback to language policy.
/// </summary>
/// <value>
/// The language fallback policy.
/// </value>
public static Fallback ToLanguage => new(new[] { Language });
/// <summary>
/// Gets the fallback to tree ancestors policy.
/// </summary>
/// <value>
/// The tree ancestors fallback policy.
/// </value>
public static Fallback ToAncestors => new(new[] { Ancestors });
/// <inheritdoc />

View File

@@ -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<T>(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<T>(this, defaultCulture, segment);
return true;
}
return false;
}
private bool TryGetValueWithDefaultLanguageFallback<T>(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<T>(this, alias, defaultCulture, segment);
return true;
}
return false;
}
}

View File

@@ -3,27 +3,33 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models;
/// <inheritdoc />
[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;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoDomain" /> class.
/// </summary>
/// <param name="domainName">The name of the domain.</param>
public UmbracoDomain(string domainName)
=> _domainName = domainName;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoDomain" /> class.
/// </summary>
/// <param name="domainName">The name of the domain.</param>
/// <param name="languageIsoCode">The language ISO code.</param>
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;
/// <inheritdoc />
[DataMember]
public string DomainName
{
@@ -31,17 +37,33 @@ public class UmbracoDomain : EntityBase, IDomain
set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName));
}
/// <inheritdoc />
public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*");
/// <inheritdoc />
[DataMember]
public int? LanguageId
{
get => _languageId;
set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId));
}
/// <inheritdoc />
public string? LanguageIsoCode { get; set; }
/// <inheritdoc />
[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("*");
/// <summary>
/// Readonly value of the language ISO code for the domain
/// </summary>
public string? LanguageIsoCode { get; set; }
/// <inheritdoc />
[DataMember]
public int SortOrder
{
get => _sortOrder;
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder));
}
}

View File

@@ -13,13 +13,28 @@ public class Domain
/// <param name="contentId">The identifier of the content which supports the domain.</param>
/// <param name="culture">The culture of the domain.</param>
/// <param name="isWildcard">A value indicating whether the domain is a wildcard domain.</param>
[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)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Domain" /> class.
/// </summary>
/// <param name="id">The unique identifier of the domain.</param>
/// <param name="name">The name of the domain.</param>
/// <param name="contentId">The identifier of the content which supports the domain.</param>
/// <param name="culture">The culture of the domain.</param>
/// <param name="isWildcard">A value indicating whether the domain is a wildcard domain.</param>
/// <param name="sortOrder">The sort order.</param>
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;
}
/// <summary>
@@ -33,30 +48,54 @@ public class Domain
ContentId = domain.ContentId;
Culture = domain.Culture;
IsWildcard = domain.IsWildcard;
SortOrder = domain.SortOrder;
}
/// <summary>
/// Gets the unique identifier of the domain.
/// </summary>
/// <value>
/// The unique identifier of the domain.
/// </value>
public int Id { get; }
/// <summary>
/// Gets the name of the domain.
/// </summary>
/// <value>
/// The name of the domain.
/// </value>
public string Name { get; }
/// <summary>
/// Gets the identifier of the content which supports the domain.
/// </summary>
/// <value>
/// The identifier of the content which supports the domain.
/// </value>
public int ContentId { get; }
/// <summary>
/// Gets the culture of the domain.
/// </summary>
/// <value>
/// The culture of the domain.
/// </value>
public string? Culture { get; }
/// <summary>
/// Gets a value indicating whether the domain is a wildcard domain.
/// </summary>
/// <value>
/// <c>true</c> if this is a wildcard domain; otherwise, <c>false</c>.
/// </value>
public bool IsWildcard { get; }
/// <summary>
/// Gets the sort order.
/// </summary>
/// <value>
/// The sort order.
/// </value>
public int SortOrder { get; }
}

View File

@@ -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<DomainAndUri> 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<DomainAndUri> baseDomains = SelectByBase(considerForBaseDomains, uri, culture);
if (baseDomains.Count > 0) // found, return
// we need to order so example.com/foo matches before example.com/
IReadOnlyCollection<DomainAndUri> 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<DomainAndUri>? SelectByCulture(IReadOnlyCollection<DomainAndUri> 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<DomainAndUri> domainsAndUris, string? culture, string? defaultCulture)
private static DomainAndUri? GetByCulture(IReadOnlyCollection<DomainAndUri> 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();
}
/// <summary>
@@ -313,14 +315,10 @@ namespace Umbraco.Cms.Core.Routing
/// <param name="domains">The domains.</param>
/// <param name="uri">The uri, or null.</param>
/// <returns>The domains and their normalized uris, that match the specified uri.</returns>
internal static IEnumerable<DomainAndUri> SelectDomains(IEnumerable<Domain> domains, Uri uri)
{
[return: NotNullIfNotNull(nameof(domains))]
private static IEnumerable<DomainAndUri>? SelectDomains(IEnumerable<Domain>? 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));
/// <summary>
/// Parses a domain name into a URI.
@@ -351,9 +349,7 @@ namespace Umbraco.Cms.Core.Routing
/// <returns>A value indicating if there is another domain defined down in the path.</returns>
/// <remarks>Looks _under_ rootNodeId but not _at_ rootNodeId.</remarks>
internal static bool ExistsDomainInPath(IEnumerable<Domain> domains, string path, int? rootNodeId)
{
return FindDomainInPath(domains, path, rootNodeId) != null;
}
=> FindDomainInPath(domains, path, rootNodeId) is not null;
/// <summary>
/// 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
/// <returns>The deepest non-wildcard Domain in the path, or null.</returns>
/// <remarks>Looks _under_ rootNodeId but not _at_ rootNodeId.</remarks>
internal static Domain? FindDomainInPath(IEnumerable<Domain> 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);
/// <summary>
/// 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
/// <returns>The deepest wildcard Domain in the path, or null.</returns>
/// <remarks>Looks _under_ rootNodeId but not _at_ rootNodeId.</remarks>
public static Domain? FindWildcardDomainInPath(IEnumerable<Domain>? domains, string path, int? rootNodeId)
=> FindDomainInPath(domains, path, rootNodeId, true);
private static Domain? FindDomainInPath(IEnumerable<Domain>? 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);
}
/// <summary>

View File

@@ -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<OperationResult?> Sort(IEnumerable<IDomain> 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);

View File

@@ -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<IDomain> GetAssignedDomains(int contentId, bool includeWildcards);
Attempt<OperationResult?> Save(IDomain domainEntity);
Attempt<OperationResult?> Sort(IEnumerable<IDomain> items)
=> Attempt.Fail(new OperationResult(OperationResultType.Failed, new EventMessages())); // TODO Remove default implmentation in a future version
}

View File

@@ -1,66 +1,46 @@
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;
/// <summary>
/// Represents the Umbraco CMS migration plan.
/// </summary>
/// <seealso cref="MigrationPlan" />
/// <seealso cref="Umbraco.Cms.Infrastructure.Migrations.MigrationPlan" />
public class UmbracoPlan : MigrationPlan
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoPlan" /> class.
/// </summary>
/// <param name="umbracoVersion">The Umbraco version.</param>
public UmbracoPlan(IUmbracoVersion umbracoVersion)
public UmbracoPlan(IUmbracoVersion umbracoVersion) // TODO (V12): Remove unused parameter
: base(Constants.Conventions.Migrations.UmbracoUpgradePlanName)
{
DefinePlan();
}
=> DefinePlan();
/// <inheritdoc />
/// <remarks>
/// <para>The default initial state in plans is string.Empty.</para>
/// <para>
/// 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.
/// </para>
/// <para>
/// This is also where we detect the current version, and reject invalid
/// upgrades (from a tool old version, or going back in time, etc).
/// </para>
/// This is set to the final migration state of 9.4, making that the lowest supported version to upgrade from.
/// </remarks>
public override string InitialState => "{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}";
/// <summary>
/// Defines the plan.
/// </summary>
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<ChangeA>("state-a")
// .To<ChangeB>("state-b") // Some might already be in this state, without having applied ChangeA
// From("state-1")
// To<ChangeA>("state-a")
// To<ChangeB>("state-b") // Some might already be in this state, without having applied ChangeA
//
// .From("state-1")
// From("state-1")
// .Merge()
// .To<ChangeA>("state-a")
// .With()
@@ -69,12 +49,12 @@ public class UmbracoPlan : MigrationPlan
From(InitialState);
// TO 10.0.0
To<AddMemberPropertiesAsColumns>("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}");
// To 10.0.0
To<V_10_0_0.AddMemberPropertiesAsColumns>("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}");
// TO 10.2.0
To<AddUserGroup2LanguageTable>("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}");
To<AddHasAccessToAllLanguagesColumn>("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}");
// To 10.2.0
To<V_10_2_0.AddUserGroup2LanguageTable>("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}");
To<V_10_2_0.AddHasAccessToAllLanguagesColumn>("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}");
// To 10.3.0
To<V_10_3_0.AddBlockGridPartialViews>("{56833770-3B7E-4FD5-A3B6-3416A26A7A3F}");
@@ -82,7 +62,10 @@ public class UmbracoPlan : MigrationPlan
// To 10.4.0
To<V_10_4_0.AddBlockGridPartialViews>("{3F5D492A-A3DB-43F9-A73E-9FEE3B180E6C}");
// to 10.5.0 / 11.2.0
To<AddPrimaryKeyConstrainToContentVersionCleanupDtos>("{83AF7945-DADE-4A02-9041-F3F6EBFAC319}");
// To 10.5.0 / 11.2.0
To<V_10_5_0.AddPrimaryKeyConstrainToContentVersionCleanupDtos>("{83AF7945-DADE-4A02-9041-F3F6EBFAC319}");
// To 11.3.0
To<V_11_3_0.AddDomainSortOrder>("{BB3889ED-E2DE-49F2-8F71-5FD8616A2661}");
}
}

View File

@@ -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<DomainDto> domainDtos = Database.Fetch<DomainDto>($"SELECT * FROM {DomainDto.TableName}");
Delete.Table(DomainDto.TableName).Do();
Create.Table<DomainDto>().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);
}
}
}

View File

@@ -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!;
/// <summary>
/// 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.
/// </summary>
[ResultColumn("languageISOCode")]
public string IsoCode { get; set; } = null!;
[Column("sortOrder")]
public int SortOrder { get; set; }
}

View File

@@ -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;
}
}

View File

@@ -18,5 +18,6 @@ public sealed class DomainMapper : BaseMapper
DefineMap<UmbracoDomain, DomainDto>(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId));
DefineMap<UmbracoDomain, DomainDto>(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage));
DefineMap<UmbracoDomain, DomainDto>(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName));
DefineMap<UmbracoDomain, DomainDto>(nameof(UmbracoDomain.SortOrder), nameof(DomainDto.SortOrder));
}
}

View File

@@ -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<int, IDomain>, IDomainRepository
{
public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger<DomainRepository> 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<IDomain> GetAll(bool includeWildcards) =>
GetMany().Where(x => includeWildcards || x.IsWildcard == false);
public IEnumerable<IDomain> GetAll(bool includeWildcards)
=> GetMany().Where(x => includeWildcards || x.IsWildcard == false);
public IEnumerable<IDomain> GetAssignedDomains(int contentId, bool includeWildcards) =>
GetMany()
.Where(x => x.RootContentId == contentId)
.Where(x => includeWildcards || x.IsWildcard == false);
public IEnumerable<IDomain> GetAssignedDomains(int contentId, bool includeWildcards)
=> GetMany().Where(x => x.RootContentId == contentId).Where(x => includeWildcards || x.IsWildcard == false);
protected override IRepositoryCachePolicy<IDomain, int> CreateCachePolicy() =>
new FullDataSetRepositoryCachePolicy<IDomain, int>(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/
false);
protected override IRepositoryCachePolicy<IDomain, int> CreateCachePolicy()
=> new FullDataSetRepositoryCachePolicy<IDomain, int>(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<IDomain> PerformGetAll(params int[]? ids)
{
@@ -49,12 +45,13 @@ internal class DomainRepository : EntityRepositoryBase<int, IDomain>, IDomainRep
{
sql.WhereIn<DomainDto>(x => x.Id, ids);
}
sql.OrderBy<DomainDto>(dto => dto.SortOrder);
return Database.Fetch<DomainDto>(sql).Select(ConvertFromDto);
return Database.Fetch<DomainDto>(sql).Select(DomainFactory.BuildEntity);
}
protected override IEnumerable<IDomain> PerformGetByQuery(IQuery<IDomain> query) =>
throw new NotSupportedException("This repository does not support this method");
protected override IEnumerable<IDomain> PerformGetByQuery(IQuery<IDomain> query)
=> throw new NotSupportedException("This repository does not support this method");
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
{
@@ -65,7 +62,7 @@ internal class DomainRepository : EntityRepositoryBase<int, IDomain>, IDomainRep
}
else
{
sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode")
sql.Select($"{Constants.DatabaseSchema.Tables.Domain}.*, {Constants.DatabaseSchema.Tables.Language}.languageISOCode")
.From<DomainDto>()
.LeftJoin<LanguageDto>()
.On<DomainDto, LanguageDto>(dto => dto.DefaultLanguage, dto => dto.Id);
@@ -74,23 +71,23 @@ internal class DomainRepository : EntityRepositoryBase<int, IDomain>, 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<string> GetDeleteClauses()
=> new []
{
var list = new List<string> { "DELETE FROM umbracoDomain WHERE id = @id" };
return list;
}
$"DELETE FROM {Constants.DatabaseSchema.Tables.Domain} WHERE id = @id",
};
protected override void PersistNewItem(IDomain entity)
{
var exists = Database.ExecuteScalar<int>(
"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<int, IDomain>, 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<int>(
"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<string>(
"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<int, IDomain>, IDomainRep
{
entity.UpdatingEntity();
// Ensure there is no other domain with the same name on another entity
var exists = Database.ExecuteScalar<int>(
"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<int, IDomain>, 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<int>(
"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<string>(
"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<int>(
$"SELECT COALESCE(MAX(sortOrder), -1) + 1 FROM {Constants.DatabaseSchema.Tables.Domain} WHERE domainRootStructureID = @rootContentId AND NOT (domainName = '' OR domainName LIKE '*%')",
new { rootContentId });
}

View File

@@ -31,7 +31,7 @@ public class DomainCache : IDomainCache
list = list.Where(x => x.IsWildcard == false);
}
return list;
return list.OrderBy(x => x.SortOrder);
}
/// <inheritdoc />
@@ -46,7 +46,7 @@ public class DomainCache : IDomainCache
list = list.Where(x => x.IsWildcard == false);
}
return list;
return list.OrderBy(x => x.SortOrder);
}
/// <inheritdoc />

View File

@@ -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);
}

View File

@@ -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<DomainDisplay>? 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<DomainDisplay> 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<OperationResult?> saveAttempt = _domainService.Save(wildcard);
if (saveAttempt == false)
Attempt<OperationResult?> 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<IDomain>())
// Process domains
if (model.Domains is not null)
{
_domainService.Delete(domain);
}
var names = new List<string>();
// create or update domains in the model
foreach (DomainDisplay domainModel in model.Domains?.Where(m => string.IsNullOrWhiteSpace(m.Name) == false) ??
Array.Empty<DomainDisplay>())
var savedDomains = new List<IDomain>();
foreach (DomainDisplay domainDisplay in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false))
{
language = languages.FirstOrDefault(l => l.Id == domainModel.Lang);
language = languages.FirstOrDefault(l => l.Id == domainDisplay.Lang);
if (language == null)
{
continue;
}
var name = domainModel.Name.ToLowerInvariant();
if (names.Contains(name))
var domainName = domainDisplay.Name.ToLowerInvariant();
if (savedDomains.Any(d => d.DomainName == domainName))
{
domainModel.Duplicate = true;
domainDisplay.Duplicate = true;
continue;
}
names.Add(name);
IDomain? domain = domains?.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name));
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)
{
var breadcrumbs = new List<string?>();
IContent? content = _contentService.GetById(rootContentId);
while (content is not null)
{
breadcrumbs.Add(content.Name);
if (content.ParentId < -1)
{
breadcrumbs.Add("Recycle Bin");
}
content = _contentService.GetParent(content);
}
breadcrumbs.Reverse();
domainDisplay.Other = "/" + string.Join("/", breadcrumbs);
}
continue;
}
// Update or create domain
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)
{
IContent? xcontent = _contentService.GetById(xrcid.Value);
var xnames = new List<string>();
while (xcontent != null)
{
if (xcontent.Name is not null)
{
xnames.Add(xcontent.Name);
}
if (xcontent.ParentId < -1)
{
xnames.Add("Recycle Bin");
}
xcontent = _contentService.GetParent(xcontent);
}
xnames.Reverse();
domainModel.Other = "/" + string.Join("/", xnames);
}
}
else
{
// yet there is a race condition here...
var newDomain = new UmbracoDomain(name) { LanguageId = domainModel.Lang, RootContentId = model.NodeId };
Attempt<OperationResult?> saveAttempt = _domainService.Save(newDomain);
if (saveAttempt == false)
domain = new UmbracoDomain(domainName)
{
LanguageId = language.Id,
RootContentId = model.NodeId,
};
}
Attempt<OperationResult?> 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;

View File

@@ -88,14 +88,42 @@ 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';
function ButtonGroupDirective() {
function link(scope) {
function controller($scope) {
$scope.toggleStyle = null;
$scope.blockElement = false;
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;
}
}
}
}
function link(scope) {
scope.dropdown = {
isOpen: false
};
@@ -112,13 +140,13 @@ Use this directive to render a button with a dropdown of alternative actions.
subButton.handler();
scope.closeDropdown();
};
}
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/buttons/umb-button-group.html',
controller: controller,
scope: {
defaultButton: "=",
subButtons: "=",
@@ -139,5 +167,4 @@ Use this directive to render a button with a dropdown of alternative actions.
}
angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective);
})();

View File

@@ -198,3 +198,19 @@
.btn-group-vertical > .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;
}
}

View File

@@ -577,11 +577,6 @@ div.help {
margin-top: 5px;
}
table.domains .help-inline {
color:@red;
}
// INPUT GROUPS
// ------------

View File

@@ -1,4 +1,4 @@
<div class="btn-group umb-button-group" ng-class="{ 'dropup': direction === 'up', '-with-button-group-toggle': subButtons.length > 0 }">
<div class="btn-group umb-button-group" ng-class="{ 'dropup': direction === 'up', '-with-button-group-toggle': subButtons.length > 0, 'btn-group-justified': blockElement }">
<umb-button
ng-if="defaultButton"
alias="{{defaultButton.alias ? defaultButton.alias : 'groupPrimary' }}"
@@ -21,7 +21,7 @@
<button
type="button"
data-element="button-group-toggle"
class="btn btn-{{buttonStyle}} dropdown-toggle umb-button-group__toggle umb-button--{{size}}"
class="btn {{toggleStyle}} dropdown-toggle umb-button-group__toggle {{size ? 'umb-button--' + size : undefined}}"
ng-if="subButtons.length > 0"
ng-click="toggleDropdown()"
aria-haspopup="true"

View File

@@ -1,21 +1,17 @@
<div ng-controller="Umbraco.Editors.Content.AssignDomainController as vm" ng-cloak>
<umb-load-indicator ng-show="vm.loading"></umb-load-indicator>
<form name="vm.domainForm" ng-submit="vm.save()" id="assignDomain" novalidate>
<div ng-hide="vm.loading" class="umb-dialog-body">
<umb-pane ng-if="!currentNode.metaData.variesByCulture">
<h5 class="umb-pane-title"><localize key="assignDomain_setLanguage">Culture</localize></h5>
<label for="assignDomain_language" class="control-label"><localize key="general_language">Language</localize></label>
<select class="umb-property-editor umb-dropdown" name="language" id="assignDomain_language" ng-model="vm.language" ng-options="lang.name for lang in vm.languages">
<option value="">{{vm.inherit}}</option>
<option value=""><localize key="assignDomain_inherit">Inherit</localize></option>
</select>
</umb-pane>
<umb-pane>
<div ng-show="vm.error">
<div class="alert alert-error">
<div><strong>{{vm.error.errorMsg}}</strong></div>
@@ -24,94 +20,48 @@
</div>
<h5 class="umb-pane-title"><localize key="assignDomain_setDomains">Domains</localize></h5>
<small class="db mb3">
<localize key="assignDomain_domainHelpWithVariants">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".</localize>
</small>
<div class="umb-el-wrap hidelabel">
<table class="table table-condensed table-bordered domains mb3" ng-if="vm.domains.length > 0">
<thead>
<tr>
<th>
<localize key="assignDomain_domain">Domain</localize>
<span class="umb-control-required">*</span>
</th>
<th>
<localize key="assignDomain_language">Language</localize>
<span class="umb-control-required">*</span>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="domain in vm.domains">
<td>
<p><small><localize key="assignDomain_domainHelpWithVariants">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".</localize></small></p>
<input type="text" class="w-100" ng-model="domain.name" name="domain_name_{{$index}}" required umb-auto-focus />
<span ng-if="vm.domainForm.$submitted" ng-messages="vm.domainForm['domain_name_' + $index].$error">
<span class="help-inline" ng-message="required"><localize key="validation_invalidEmpty">Value cannot be empty</localize></span>
</span>
<span ng-show="domain.duplicate" class="help-inline"><localize key="assignDomain_duplicateDomain">Domain has already been assigned.</localize><span ng-show="domain.other">({{domain.other}})</span></span>
</td>
<td>
<select
name="domain_language_{{$index}}"
class="language w-100"
ng-model="domain.lang"
ng-options="lang.name for lang in vm.languages"
required>
</select>
</td>
<td>
<umb-button
icon="icon-trash"
action="vm.removeDomain($index)"
type="button"
button-style="danger">
</umb-button>
</td>
</tr>
</tbody>
</table>
<div class="form-horizontal" ui-sortable="sortableOptions" ng-model="vm.domains">
<div ng-repeat="domain in vm.domains">
<div ng-if="domain.duplicate" class="alert alert-error property-error">
<localize key="assignDomain_duplicateDomain">Domain has already been assigned.</localize><span ng-if="domain.other"> ({{domain.other}})</span>
</div>
<umb-button
label-key="assignDomain_addNew"
action="vm.addDomain()"
type="button"
button-style="info">
</umb-button>
<umb-button
label-key="assignDomain_addCurrent"
action="vm.addCurrentDomain()"
type="button"
button-style="success">
</umb-button>
<div ng-if="vm.domainForm.$submitted" ng-messages="vm.domainForm['domain_name_' + $index].$error">
<div class="alert alert-error property-error" ng-message="required"><localize key="validation_invalidEmpty">Value cannot be empty</localize></div>
</div>
<div class="umb-prevalues-multivalues__listitem">
<umb-icon icon="icon-navigation" class="icon handle"></umb-icon>
<div class="umb-prevalues-multivalues__left">
<input type="text" ng-model="domain.name" name="domain_name_{{$index}}" required umb-auto-focus />
<select ng-model="domain.lang" name="domain_language_{{$index}}" ng-options="lang.name for lang in vm.languages" required></select>
</div>
<div class="umb-prevalues-multivalues__right">
<button type="button" class="umb-node-preview__action" ng-click="vm.removeDomain($index)"><localize key="general_remove">Remove</localize></button>
</div>
</div>
</div>
</div>
<umb-button-group ng-if="vm.buttonGroup"
default-button="vm.buttonGroup.defaultButton"
sub-buttons="vm.buttonGroup.subButtons"
button-style="[placeholder, block]"
float="right">
</umb-button-group>
</umb-pane>
</div>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<umb-button
label-key="general_close"
<umb-button label-key="general_close"
action="vm.closeDialog()"
type="button"
button-style="link">
</umb-button>
<umb-button
label-key="buttons_save"
<umb-button label-key="buttons_save"
type="submit"
button-style="success"
state="vm.submitButtonState">
</umb-button>
</div>
</form>
</div>

View File

@@ -4,19 +4,38 @@
function AssignDomainController($scope, localizationService, languageResource, contentResource, navigationService, notificationsService, $location) {
var vm = this;
vm.loading = true;
vm.closeDialog = closeDialog;
vm.addDomain = addDomain;
vm.addCurrentDomain = addCurrentDomain;
vm.removeDomain = removeDomain;
vm.save = save;
vm.languages = [];
vm.domains = [];
vm.language = null;
vm.buttonGroup = {
defaultButton: {
labelKey: 'assignDomain_addNew',
buttonStyle: 'info',
handler: addDomain
},
subButtons: [{
labelKey: 'assignDomain_addCurrent',
buttonStyle: 'success',
handler: addCurrentDomain
}]
};
$scope.sortableOptions = {
axis: 'y',
containment: 'parent',
cursor: 'move',
handle: ".handle",
placeholder: 'sortable-placeholder',
forcePlaceholderSize: true,
tolerance: 'pointer'
};
function activate() {
vm.loading = true;
languageResource.getAll().then(langs => {
vm.languages = langs;
@@ -30,25 +49,13 @@
else {
vm.defaultLanguage = langs[0];
}
getCultureAndDomains().then(() => {
vm.loading = false;
});
});
localizationService.localize("assignDomain_inherit").then(function (value) {
vm.inherit = value;
});
}
function getCultureAndDomains () {
return contentResource.getCultureAndDomains($scope.currentNode.id)
.then(function (data) {
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];
}
@@ -58,17 +65,22 @@
var matchedLangs = vm.languages.filter(function (lng) {
return matchLanguageById(lng, d.lang);
});
return {
name: d.name,
lang: matchedLangs.length > 0 ? matchedLangs[0] : vm.defaultLanguage
}
});
vm.loading = false;
});
});
}
function matchLanguageById(language, id) {
var langId = parseInt(language.id);
var comparisonId = parseInt(id);
return langId === comparisonId;
}
@@ -89,6 +101,7 @@
if (port != 80 && port != 443) {
domainToAdd += ":" + port;
}
vm.domains.push({
name: domainToAdd,
lang: vm.defaultLanguage
@@ -100,12 +113,10 @@
}
function save() {
vm.error = null;
vm.submitButtonState = "busy";
if (vm.domainForm.$valid) {
// clear validation messages
vm.domains.forEach(domain => {
domain.duplicate = null;
@@ -124,17 +135,17 @@
};
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
closeDialog();
} else {
// show validation messages for each domain
response.domains.forEach(validation => {
vm.domains.forEach(domain => {
if (validation.name === domain.name) {
@@ -143,24 +154,25 @@
}
});
});
vm.submitButtonState = "error";
localizationService.localize('speechBubbles_editCulturesAndHostnamesError').then(function (value) {
notificationsService.error(value);
});
}
}, function (e) {
vm.error = e;
vm.submitButtonState = "error";
});
}
else {
} else {
vm.submitButtonState = "error";
}
}
activate();
}
angular.module("umbraco").controller("Umbraco.Editors.Content.AssignDomainController", AssignDomainController);
})();

View File

@@ -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);

View File

@@ -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());
}
}

View File

@@ -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/");
}
}

View File

@@ -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<string, string> headers = new Dictionary<string, string> { ["Hello"] = "world" };
var redirect = "https://test.com";

View File

@@ -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()
{