Merge remote-tracking branch 'origin/temp8' into temp8-222-variant-invariant-data-updates

# Conflicts:
#	src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs
This commit is contained in:
Shannon
2018-10-04 16:39:23 +02:00
79 changed files with 6025 additions and 1987 deletions

View File

@@ -159,6 +159,9 @@ namespace Umbraco.Core.Composing
public static IPublishedValueFallback PublishedValueFallback
=> _publishedValueFallback ?? Container.GetInstance<IPublishedValueFallback>() ?? new NoopPublishedValueFallback();
public static IVariationContextAccessor VariationContextAccessor
=> Container.GetInstance<IVariationContextAccessor>();
#endregion
}
}

View File

@@ -233,7 +233,7 @@ namespace Umbraco.Core.Migrations.Install
private void CreateLanguageData()
{
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefaultVariantLanguage = true });
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true });
}
private void CreateContentChildTypeData()

View File

@@ -107,18 +107,35 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
//Chain<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove,
Chain<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but it after shannon's, with a new target state,
Chain<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state,
Add<UserForeignKeys>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state
// resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ...
Chain<ContentVariationMigration>("{1350617A-4930-4D61-852F-E3AA9E692173}");
Chain<UpdateUmbracoConsent>("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
//Chain<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
Chain<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
Chain<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
Chain<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
Chain<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
//Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
Chain<FallbackLanguage>("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's
.CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next
// resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ...
Chain<UpdateDefaultMandatoryLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state
From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's
.Chain<FallbackLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
//Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
Chain<RefactorVariantsModel>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}")
.Chain<FallbackLanguage>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
//FINAL

View File

@@ -0,0 +1,24 @@
using System.Linq;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
/// <summary>
/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for
/// a given language.
/// </summary>
public class FallbackLanguage : MigrationBase
{
public FallbackLanguage(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false)
AddColumn<LanguageDto>("fallbackLanguageId");
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
var defaultId = int.MaxValue;
foreach (var dto in dtos)
{
if (dto.IsDefaultVariantLanguage)
if (dto.IsDefault)
{
defaultId = dto.Id;
break;
@@ -38,8 +38,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// update, so that language with that id is now default and mandatory
var updateDefault = Sql()
.Update<LanguageDto>(u => u
.Set(x => x.IsDefaultVariantLanguage, true)
.Set(x => x.Mandatory, true))
.Set(x => x.IsDefault, true)
.Set(x => x.IsMandatory, true))
.Where<LanguageDto>(x => x.Id == defaultId);
Database.Execute(updateDefault);

View File

@@ -4,34 +4,54 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents a language.
/// </summary>
public interface ILanguage : IEntity, IRememberBeingDirty
{
/// <summary>
/// Gets or sets the Iso Code for the Language
/// Gets or sets the ISO code of the language.
/// </summary>
[DataMember]
string IsoCode { get; set; }
/// <summary>
/// Gets or sets the Culture Name for the Language
/// Gets or sets the culture name of the language.
/// </summary>
[DataMember]
string CultureName { get; set; }
/// <summary>
/// Returns a <see cref="CultureInfo"/> object for the current Language
/// Gets the <see cref="CultureInfo"/> object for the language.
/// </summary>
[IgnoreDataMember]
CultureInfo CultureInfo { get; }
/// <summary>
/// Defines if this language is the default variant language when language variants are in use
/// Gets or sets a value indicating whether the language is the default language.
/// </summary>
bool IsDefaultVariantLanguage { get; set; }
[DataMember]
bool IsDefault { get; set; }
/// <summary>
/// If true, a variant node cannot be published unless this language variant is created
/// Gets or sets a value indicating whether the language is mandatory.
/// </summary>
bool Mandatory { get; set; }
/// <remarks>
/// <para>When a language is mandatory, a multi-lingual document cannot be published
/// without that language being published, and unpublishing that language unpublishes
/// the entire document.</para>
/// </remarks>
[DataMember]
bool IsMandatory { get; set; }
/// <summary>
/// Gets or sets the identifier of a fallback language.
/// </summary>
/// <remarks>
/// <para>The fallback language can be used in multi-lingual scenarios, to help
/// define fallback strategies when a value does not exist for a requested language.</para>
/// </remarks>
[DataMember]
int? FallbackLanguageId { get; set; }
}
}

View File

@@ -19,6 +19,7 @@ namespace Umbraco.Core.Models
private string _cultureName;
private bool _isDefaultVariantLanguage;
private bool _mandatory;
private int? _fallbackLanguageId;
public Language(string isoCode)
{
@@ -30,13 +31,12 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo IsoCodeSelector = ExpressionHelper.GetPropertyInfo<Language, string>(x => x.IsoCode);
public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo<Language, string>(x => x.CultureName);
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefaultVariantLanguage);
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.Mandatory);
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefault);
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsMandatory);
public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, int?>(x => x.FallbackLanguageId);
}
/// <summary>
/// Gets or sets the Iso Code for the Language
/// </summary>
/// <inheritdoc />
[DataMember]
public string IsoCode
{
@@ -44,9 +44,7 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector);
}
/// <summary>
/// Gets or sets the Culture Name for the Language
/// </summary>
/// <inheritdoc />
[DataMember]
public string CultureName
{
@@ -54,22 +52,29 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector);
}
/// <summary>
/// Returns a <see cref="CultureInfo"/> object for the current Language
/// </summary>
/// <inheritdoc />
[IgnoreDataMember]
public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode);
public bool IsDefaultVariantLanguage
/// <inheritdoc />
public bool IsDefault
{
get => _isDefaultVariantLanguage;
set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector);
}
public bool Mandatory
/// <inheritdoc />
public bool IsMandatory
{
get => _mandatory;
set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector);
}
/// <inheritdoc />
public int? FallbackLanguageId
{
get => _fallbackLanguageId;
set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector);
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Manages the built-in fallback policies.
/// </summary>
public struct Fallback : IEnumerable<int>
{
private readonly int[] _values;
/// <summary>
/// Initializes a new instance of the <see cref="Fallback"/> struct with values.
/// </summary>
private Fallback(int[] values)
{
_values = values;
}
/// <summary>
/// Gets an ordered set of fallback policies.
/// </summary>
/// <param name="values"></param>
public static Fallback To(params int[] values) => new Fallback(values);
/// <summary>
/// Do not fallback.
/// </summary>
public const int None = 0;
/// <summary>
/// Fallback to default value.
/// </summary>
public const int DefaultValue = 1;
/// <summary>
/// Gets the fallback to default value policy.
/// </summary>
public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue });
/// <summary>
/// Fallback to other languages.
/// </summary>
public const int Language = 2;
/// <summary>
/// Gets the fallback to language policy.
/// </summary>
public static Fallback ToLanguage => new Fallback(new[] { Language });
/// <summary>
/// Fallback to tree ancestors.
/// </summary>
public const int Ancestors = 3;
/// <summary>
/// Gets the fallback to tree ancestors policy.
/// </summary>
public static Fallback ToAncestors => new Fallback(new[] { Ancestors });
/// <inheritdoc />
public IEnumerator<int> GetEnumerator()
{
return ((IEnumerable<int>)_values ?? Array.Empty<int>()).GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -1,37 +1,122 @@
using Umbraco.Core.Composing;
namespace Umbraco.Core.Models.PublishedContent
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides a fallback strategy for getting <see cref="IPublishedElement"/> values.
/// </summary>
// fixme - IPublishedValueFallback is still WorkInProgress
// todo - properly document methods, etc
// todo - understand caching vs fallback (recurse etc)
public interface IPublishedValueFallback
{
// note that at property level, property.GetValue() does NOT implement fallback, and one has
// to get property.Value() or property.Value<T>() to trigger fallback
/// <summary>
/// Tries to get a fallback value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever property.Value(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.</para>
/// <para>It can only fallback at property level (no recurse).</para>
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
/// <para>Note that <paramref name="culture"/> and <paramref name="segment"/> may not be contextualized,
/// so the variant context should be used to contextualize them (see our default implementation in
/// the web project.</para>
/// </remarks>
bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// this method is called whenever property.Value(culture, segment, defaultValue) is called, and
// property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse).
/// <summary>
/// Tries to get a fallback value for a property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.</para>
/// <para>It can only fallback at property level (no recurse).</para>
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
/// </remarks>
bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value);
object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue);
/// <summary>
/// Tries to get a fallback value for a published element property.
/// </summary>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// <para>It can only fallback at element level (no recurse).</para>
/// </remarks>
bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// this method is called whenever property.Value<T>(culture, segment, defaultValue) is called, and
// property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse).
/// <summary>
/// Tries to get a fallback value for a published element property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// <para>It can only fallback at element level (no recurse).</para>
/// </remarks>
bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue);
/// <summary>
/// Tries to get a fallback value for a published content property.
/// </summary>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// </remarks>
bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// these methods to be called whenever getting the property value for the specified alias, culture and segment,
// either returned no property at all, or a property that does not HasValue for the specified culture and segment.
object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue);
T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue);
object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse);
T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse);
/// <summary>
/// Tries to get a fallback value for a published content property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// </remarks>
bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
}
}

View File

@@ -49,17 +49,24 @@ namespace Umbraco.Core.Models.PublishedContent
/// <param name="modelTypes">The model types map.</param>
/// <returns>The actual Clr type.</returns>
public static Type Map(Type type, Dictionary<string, Type> modelTypes)
=> Map(type, modelTypes, false);
public static Type Map(Type type, Dictionary<string, Type> modelTypes, bool dictionaryIsInvariant)
{
// it may be that senders forgot to send an invariant dictionary (garbage-in)
if (!dictionaryIsInvariant)
modelTypes = new Dictionary<string, Type>(modelTypes, StringComparer.InvariantCultureIgnoreCase);
if (type is ModelType modelType)
{
if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out Type actualType))
if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out var actualType))
return actualType;
throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\".");
}
if (type is ModelTypeArrayType arrayType)
{
if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out Type actualType))
if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out var actualType))
return actualType.MakeArrayType();
throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\".");
}
@@ -70,7 +77,7 @@ namespace Umbraco.Core.Models.PublishedContent
if (def == null)
throw new InvalidOperationException("panic");
var args = type.GetGenericArguments().Select(x => Map(x, modelTypes)).ToArray();
var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray();
return def.MakeGenericType(args);
}
@@ -81,7 +88,14 @@ namespace Umbraco.Core.Models.PublishedContent
/// <param name="map">The model types map.</param>
/// <returns>The actual Clr type name.</returns>
public static string MapToName(Type type, Dictionary<string, string> map)
=> MapToName(type, map, false);
private static string MapToName(Type type, Dictionary<string, string> map, bool dictionaryIsInvariant)
{
// it may be that senders forgot to send an invariant dictionary (garbage-in)
if (!dictionaryIsInvariant)
map = new Dictionary<string, string>(map, StringComparer.InvariantCultureIgnoreCase);
if (type is ModelType modelType)
{
if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName))
@@ -102,7 +116,7 @@ namespace Umbraco.Core.Models.PublishedContent
if (def == null)
throw new InvalidOperationException("panic");
var args = type.GetGenericArguments().Select(x => MapToName(x, map)).ToArray();
var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray();
var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`'));
return defFullName + "<" + string.Join(", ", args) + ">";
}

View File

@@ -9,21 +9,45 @@
public class NoopPublishedValueFallback : IPublishedValueFallback
{
/// <inheritdoc />
public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue;
public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue;
public bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
/// <inheritdoc />
public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue;
public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue;
public bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
/// <inheritdoc />
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue;
public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue;
public bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
}
}
}

View File

@@ -0,0 +1,15 @@
namespace Umbraco.Core.Models.PublishedContent
{
public static class VariationContextAccessorExtensions
{
public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string culture, ref string segment)
{
if (culture != null && segment != null) return;
// use context values
var publishedVariationContext = variationContextAccessor?.VariationContext;
if (culture == null) culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : "";
if (segment == null) segment = variations.VariesBySegment() ? publishedVariationContext?.Segment : "";
}
}
}

View File

@@ -10,33 +10,51 @@ namespace Umbraco.Core.Persistence.Dtos
{
public const string TableName = Constants.DatabaseSchema.Tables.Language;
/// <summary>
/// Gets or sets the identifier of the language.
/// </summary>
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 2)]
public short Id { get; set; }
/// <summary>
/// Gets or sets the ISO code of the language.
/// </summary>
[Column("languageISOCode")]
[Index(IndexTypes.UniqueNonClustered)]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(14)]
public string IsoCode { get; set; }
/// <summary>
/// Gets or sets the culture name of the language.
/// </summary>
[Column("languageCultureName")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(100)]
public string CultureName { get; set; }
/// <summary>
/// Defines if this language is the default variant language when language variants are in use
/// Gets or sets a value indicating whether the language is the default language.
/// </summary>
[Column("isDefaultVariantLang")]
[Constraint(Default = "0")]
public bool IsDefaultVariantLanguage { get; set; }
public bool IsDefault { get; set; }
/// <summary>
/// If true, a variant node cannot be published unless this language variant is created
/// Gets or sets a value indicating whether the language is mandatory.
/// </summary>
[Column("mandatory")]
[Constraint(Default = "0")]
public bool Mandatory { get; set; }
public bool IsMandatory { get; set; }
/// <summary>
/// Gets or sets the identifier of a fallback language.
/// </summary>
[Column("fallbackLanguageId")]
[ForeignKey(typeof(LanguageDto), Column = "id")]
[Index(IndexTypes.NonClustered)]
[NullSetting(NullSetting = NullSettings.Null)]
public int? FallbackLanguageId { get; set; }
}
}

View File

@@ -12,8 +12,9 @@ namespace Umbraco.Core.Persistence.Factories
{
CultureName = dto.CultureName,
Id = dto.Id,
IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage,
Mandatory = dto.Mandatory
IsDefault = dto.IsDefault,
IsMandatory = dto.IsMandatory,
FallbackLanguageId = dto.FallbackLanguageId
};
// reset dirty initial properties (U4-1946)
@@ -27,12 +28,15 @@ namespace Umbraco.Core.Persistence.Factories
{
CultureName = entity.CultureName,
IsoCode = entity.IsoCode,
IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage,
Mandatory = entity.Mandatory
IsDefault = entity.IsDefault,
IsMandatory = entity.IsMandatory,
FallbackLanguageId = entity.FallbackLanguageId
};
if (entity.HasIdentity)
{
dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture));
}
return dto;
}

View File

@@ -74,7 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var sqlClause = GetBaseQuery(false);
var translator = new SqlTranslator<ILanguage>(sqlClause, query);
var sql = translator.Translate();
return Database.Fetch<LanguageDto>(sql).Select(ConvertFromDto);
var dtos = Database.Fetch<LanguageDto>(sql);
return dtos.Select(ConvertFromDto).ToList();
}
#endregion
@@ -115,10 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return list;
}
protected override Guid NodeObjectTypeId
{
get { throw new NotImplementedException(); }
}
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
#endregion
@@ -133,15 +131,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
((EntityBase) entity).AddingEntity();
// deal with entity becoming the new default entity
if (entity.IsDefaultVariantLanguage)
if (entity.IsDefault)
{
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql()
.Update<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
.Update<LanguageDto>(u => u.Set(x => x.IsDefault, false));
Database.Execute(setAllDefaultToFalse);
}
;
// fallback cycles are detected at service level
// insert
var dto = LanguageFactory.BuildDto(entity);
var id = Convert.ToInt32(Database.Insert(dto));
@@ -157,14 +157,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
((EntityBase) entity).UpdatingEntity();
if (entity.IsDefaultVariantLanguage)
if (entity.IsDefault)
{
// deal with entity becoming the new default entity
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql()
.Update<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
.Update<LanguageDto>(u => u.Set(x => x.IsDefault, false));
Database.Execute(setAllDefaultToFalse);
}
else
@@ -174,13 +174,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var selectDefaultId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
.Where<LanguageDto>(x => x.IsDefault);
var defaultId = Database.ExecuteScalar<int>(selectDefaultId);
if (entity.Id == defaultId)
throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead.");
}
// fallback cycles are detected at service level
// update
var dto = LanguageFactory.BuildDto(entity);
Database.Update(dto);
@@ -195,12 +197,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var selectDefaultId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
.Where<LanguageDto>(x => x.IsDefault);
var defaultId = Database.ExecuteScalar<int>(selectDefaultId);
if (entity.Id == defaultId)
throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode}).");
// We need to remove any references to the language if it's being used as a fall-back from other ones
var clearFallbackLanguage = Sql()
.Update<LanguageDto>(u => u
.Set(x => x.FallbackLanguageId, null))
.Where<LanguageDto>(x => x.FallbackLanguageId == entity.Id);
Database.Execute(clearFallbackLanguage);
// delete
base.PersistDeletedItem(entity);
}
@@ -212,7 +222,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var entity = LanguageFactory.BuildEntity(dto);
return entity;
}
public ILanguage GetByIsoCode(string isoCode)
{
// ensure cache is populated, in a non-expensive way
@@ -287,7 +297,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests)
?? CachePolicy.GetAll(Array.Empty<int>(), PerformGetAll)).ToList();
var language = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage);
var language = languages.FirstOrDefault(x => x.IsDefault);
if (language != null) return language;
// this is an anomaly, the service/repo should ensure it cannot happen

View File

@@ -1034,7 +1034,7 @@ namespace Umbraco.Core.Services.Implement
var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published
if (!cannotBePublished)
{
var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode);
var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode);
cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published
}

View File

@@ -363,6 +363,16 @@ namespace Umbraco.Core.Services.Implement
// write-lock languages to guard against race conds when dealing with default language
scope.WriteLock(Constants.Locks.Languages);
// look for cycles - within write-lock
if (language.FallbackLanguageId.HasValue)
{
var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
if (!languages.ContainsKey(language.FallbackLanguageId.Value))
throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
if (CreatesCycle(language, languages))
throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
}
var saveEventArgs = new SaveEventArgs<ILanguage>(language);
if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs))
{
@@ -380,6 +390,20 @@ namespace Umbraco.Core.Services.Implement
}
}
private bool CreatesCycle(ILanguage language, IDictionary<int, ILanguage> languages)
{
// a new language is not referenced yet, so cannot be part of a cycle
if (!language.HasIdentity) return false;
var id = language.FallbackLanguageId;
while (true) // assuming languages does not already contains a cycle, this must end
{
if (!id.HasValue) return false; // no fallback means no cycle
if (id.Value == language.Id) return true; // back to language = cycle!
id = languages[id.Value].FallbackLanguageId; // else keep chaining
}
}
/// <summary>
/// Deletes a <see cref="ILanguage"/> by removing it (but not its usages) from the db
/// </summary>
@@ -399,8 +423,7 @@ namespace Umbraco.Core.Services.Implement
return;
}
//NOTE: There isn't any constraints in the db, so possible references aren't deleted
// NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
_languageRepository.Delete(language);
deleteEventArgs.CanCancel = false;

View File

@@ -362,6 +362,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorVariantsModel.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\SuperZero.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FallbackLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UpdateDefaultMandatoryLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UserForeignKeys.cs" />
<Compile Include="Models\AuditEntry.cs" />
@@ -391,12 +392,14 @@
<Compile Include="Models\PathValidationExtensions.cs" />
<Compile Include="Models\Entities\TreeEntityBase.cs" />
<Compile Include="Models\PropertyTagsExtensions.cs" />
<Compile Include="Models\PublishedContent\Fallback.cs" />
<Compile Include="Models\PublishedContent\NoopPublishedValueFallback.cs" />
<Compile Include="Models\PublishedContent\PublishedCultureInfos.cs" />
<Compile Include="Models\PublishedContent\IVariationContextAccessor.cs" />
<Compile Include="Models\PublishedContent\IPublishedValueFallback.cs" />
<Compile Include="Models\PublishedContent\VariationContext.cs" />
<Compile Include="Models\PublishedContent\ThreadCultureVariationContextAccessor.cs" />
<Compile Include="Models\PublishedContent\VariationContextAccessorExtensions.cs" />
<Compile Include="Persistence\Dtos\AuditEntryDto.cs" />
<Compile Include="Persistence\Dtos\ConsentDto.cs" />
<Compile Include="Persistence\Dtos\ContentVersionCultureVariationDto.cs" />