diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index e1071334d6..124dc93140 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -17,4 +17,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.0.0")] -[assembly: AssemblyInformationalVersion("8.0.0-alpha.33")] +[assembly: AssemblyInformationalVersion("8.0.0-alpha.34")] diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index f462c1b6ea..a763b22e36 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -33,6 +33,7 @@ namespace Umbraco.Core.Composing private static ILogger _logger; private static IProfiler _profiler; private static ProfilingLogger _profilingLogger; + private static IPublishedValueFallback _publishedValueFallback; /// /// Gets or sets the DI container. @@ -63,6 +64,7 @@ namespace Umbraco.Core.Composing _logger = null; _profiler = null; _profilingLogger = null; + _publishedValueFallback = null; Resetted?.Invoke(null, EventArgs.Empty); } @@ -153,6 +155,9 @@ namespace Umbraco.Core.Composing public static IPublishedContentTypeFactory PublishedContentTypeFactory => Container.GetInstance(); + public static IPublishedValueFallback PublishedValueFallback + => _publishedValueFallback ?? Container.GetInstance() ?? new NoopPublishedValueFallback(); + #endregion } } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 0e8e473cf0..063125e462 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Configuration /// /// Gets the version comment of the executing code (eg "beta"). /// - public static string CurrentComment => "alpha.33"; + public static string CurrentComment => "alpha.34"; /// /// Gets the assembly version of Umbraco.Code.dll. diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 86baccd946..7abaac3404 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -232,8 +232,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - //public IReadOnlyDictionary PublishNames => _publishNames ?? NoNames; - public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name) ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; /// public string GetPublishName(string culture) @@ -270,7 +269,7 @@ namespace Umbraco.Core.Models => !string.IsNullOrWhiteSpace(GetPublishName(culture)); /// - public DateTime GetDateCulturePublished(string culture) + public DateTime GetCulturePublishDate(string culture) { if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) return infos.Date; @@ -358,7 +357,7 @@ namespace Umbraco.Core.Models var name = GetName(culture); if (string.IsNullOrWhiteSpace(name)) return false; //fixme this should return an attempt with error results - + SetPublishInfos(culture, name, DateTime.Now); } @@ -478,8 +477,8 @@ namespace Umbraco.Core.Models // copy names ClearNames(); - foreach (var (languageId, name) in other.Names) - SetName(languageId, name); + foreach (var (culture, name) in other.Names) + SetName(name, culture); Name = other.Name; } @@ -519,7 +518,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(culture, other.GetName(culture)); + SetName(other.GetName(culture), culture); } /// @@ -553,7 +552,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(culture, other.GetName(culture)); + SetName(other.GetName(culture), culture); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 823d8bfb0a..f1220c721f 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Web; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -18,14 +19,14 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { - protected static readonly Dictionary NoNames = new Dictionary(); + protected static readonly Dictionary NoNames = new Dictionary(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; - private Dictionary _names; + private Dictionary _cultureInfos; /// /// Initializes a new instance of the class. @@ -54,8 +55,8 @@ namespace Umbraco.Core.Models // initially, all new instances have Id = 0; // no identity VersionId = 0; // no versions - - SetName(culture, name); + + SetName(name, culture); _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); @@ -67,7 +68,7 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); - public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); + public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.Names); } @@ -135,23 +136,37 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; - + #region Cultures /// [DataMember] - public virtual IReadOnlyDictionary Names + public virtual IReadOnlyDictionary Names => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + + // sets culture infos + // internal for repositories + // clear by clearing name + internal void SetCultureInfos(string culture, string name, DateTime date) { - get => _names ?? NoNames; - set + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture == null) { - foreach (var (culture, name) in value) - SetName(culture, name); + Name = name; + return; } + + // private method, assume that culture is valid + + if (_cultureInfos == null) + _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _cultureInfos[culture] = (name, date); } /// - public virtual void SetName(string culture, string name) + public virtual void SetName(string name, string culture) { if (string.IsNullOrWhiteSpace(name)) { @@ -168,10 +183,10 @@ namespace Umbraco.Core.Models if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) throw new NotSupportedException("Content type does not support varying name by culture."); - if (_names == null) - _names = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (_cultureInfos == null) + _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - _names[culture] = name; + _cultureInfos[culture] = (name, DateTime.Now) ; OnPropertyChanged(Ps.Value.NamesSelector); } @@ -179,14 +194,14 @@ namespace Umbraco.Core.Models public virtual string GetName(string culture) { if (culture == null) return Name; - if (_names == null) return null; - return _names.TryGetValue(culture, out var name) ? name : null; + if (_cultureInfos == null) return null; + return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// public bool IsCultureAvailable(string culture) => !string.IsNullOrWhiteSpace(GetName(culture)); - + private void ClearName(string culture) { if (culture == null) @@ -194,28 +209,37 @@ namespace Umbraco.Core.Models Name = null; return; } - - if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) - throw new NotSupportedException("Content type does not support varying name by culture."); - if (_names == null) return; - if (!_names.ContainsKey(culture)) - throw new InvalidOperationException($"Cannot unpublish culture {culture}, the document contains only cultures {string.Join(", ", _names.Keys)}"); - _names.Remove(culture); - if (_names.Count == 0) - _names = null; + if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) + throw new NotSupportedException("Content type does not support varying name by culture."); + + if (_cultureInfos == null) return; + if (!_cultureInfos.ContainsKey(culture)) + throw new InvalidOperationException($"Cannot unpublish culture {culture}, the document contains only cultures {string.Join(", ", _cultureInfos.Keys)}"); + + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + _cultureInfos = null; } protected virtual void ClearNames() - { + { if (!ContentTypeBase.Variations.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) - throw new NotSupportedException("Content type does not support varying name by culture."); + throw new NotSupportedException("Content type does not support varying name by culture."); - _names = null; + _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } - - #endregion + + /// + public DateTime GetCultureDate(string culture) + { + if (_cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) + return infos.Date; + throw new InvalidOperationException($"Culture \"{culture}\" is not available."); + } + + #endregion #region Has, Get, Set, Publish Property Value @@ -380,7 +404,7 @@ namespace Umbraco.Core.Models var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } - + #endregion } } diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index 163879bbe0..7808cea19b 100644 --- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs @@ -26,6 +26,16 @@ namespace Umbraco.Core.Models.Entities /// public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; + /// + /// Gets the AdditionalData key for culture names. + /// + public const string AdditionalCultureNames = "CultureNames"; + + /// + /// Gets the AdditionalData key for variations. + /// + public const string AdditionalVariations = "Variations"; + // implement IEntity diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 2f3fd06f5f..59ff548e16 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -93,7 +93,7 @@ namespace Umbraco.Core.Models /// /// Gets the date a culture was published. /// - DateTime GetDateCulturePublished(string culture); + DateTime GetCulturePublishDate(string culture); /// /// Gets a value indicated whether a given culture is edited. diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 93a6e82ada..3007eea900 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -34,7 +35,7 @@ namespace Umbraco.Core.Models /// When is null, sets the invariant /// language, which sets the property. /// - void SetName(string culture, string value); + void SetName(string value, string culture); /// /// Gets the name of the content item for a specified language. @@ -46,13 +47,13 @@ namespace Umbraco.Core.Models string GetName(string culture); /// - /// Gets or sets the names of the content item. + /// Gets the names of the content item. /// /// - /// Because a dictionary key cannot be null this cannot get nor set the invariant + /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get or set via the property. /// - IReadOnlyDictionary Names { get; set; } + IReadOnlyDictionary Names { get; } /// /// Gets a value indicating whether a given culture is available. @@ -63,6 +64,11 @@ namespace Umbraco.Core.Models /// bool IsCultureAvailable(string culture); + /// + /// Gets the date a culture was created. + /// + DateTime GetCultureDate(string culture); + /// /// List of properties, which make up all the data available for this Content object /// diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index f75e59bee0..d9c0b8a35a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -6,80 +6,172 @@ namespace Umbraco.Core.Models.PublishedContent /// /// - /// Represents a cached content. + /// Represents a published content item. /// + /// + /// Can be a published document, media or member. + /// public interface IPublishedContent : IPublishedElement { #region Content - // fixme - all these are colliding with models => ? - // or could we force them to be 'new' in models? - - int Id { get; } - int TemplateId { get; } - int SortOrder { get; } - string Name { get; } - string UrlName { get; } // fixme rename - string DocumentTypeAlias { get; } // fixme obsolete - int DocumentTypeId { get; } // fixme obsolete - string WriterName { get; } - string CreatorName { get; } - int WriterId { get; } - int CreatorId { get; } - string Path { get; } - DateTime CreateDate { get; } - DateTime UpdateDate { get; } - int Level { get; } - string Url { get; } - - IReadOnlyDictionary CultureNames { get; } + // todo - IPublishedContent properties colliding with models + // we need to find a way to remove as much clutter as possible from IPublishedContent, + // since this is preventing someone from creating a property named 'Path' and have it + // in a model, for instance. we could move them all under one unique property eg + // Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8 /// - /// Gets a value indicating whether the content is a content (aka a document) or a media. + /// Gets the unique identifier of the content item. + /// + int Id { get; } + + /// + /// Gets the name of the content item. + /// + /// + /// The value of this property is contextual. When the content type is multi-lingual, + /// this is the name for the 'current' culture. Otherwise, it is the invariant name. + /// + string Name { get; } + + /// + /// Gets the url segment of the content item. + /// + /// + /// The value of this property is contextual. When the content type is multi-lingual, + /// this is the name for the 'current' culture. Otherwise, it is the invariant url segment. + /// + string UrlSegment { get; } + + /// + /// Gets the sort order of the content item. + /// + int SortOrder { get; } + + /// + /// Gets the tree level of the content item. + /// + int Level { get; } + + /// + /// Gets the tree path of the content item. + /// + string Path { get; } + + /// + /// Gets the identifier of the template to use to render the content item. + /// + int TemplateId { get; } + + /// + /// Gets the identifier of the user who created the content item. + /// + int CreatorId { get; } + + /// + /// Gets the name of the user who created the content item. + /// + string CreatorName { get; } + + /// + /// Gets the date the content item was created. + /// + DateTime CreateDate { get; } + + /// + /// Gets the identifier of the user who last updated the content item. + /// + int WriterId { get; } + + /// + /// Gets the name of the user who last updated the content item. + /// + string WriterName { get; } + + /// + /// Gets the date the content item was last updated. + /// + /// + /// For published content items, this is also the date the item was published. + /// This date is always global to the content item, see GetCulture().Date for the + /// date each culture was published. + /// + DateTime UpdateDate { get; } + + /// + /// Gets the url of the content item. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// 'current' culture. Otherwise, it is the invariant url. + /// + string Url { get; } + + /// + /// Gets the url of the content item. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + string GetUrl(string culture = null); + + /// + /// Gets culture infos for a culture. + /// + PublishedCultureInfos GetCulture(string culture = null); + + /// + /// Gets culture infos. + /// + /// + /// Contains only those culture that are available. For a published content, these are + /// the cultures that are published. For a draft content, those that are 'available' ie + /// have a non-empty content name. + /// + IReadOnlyDictionary Cultures { get; } + + /// + /// Gets the type of the content item (document, media...). /// PublishedItemType ItemType { get; } /// /// Gets a value indicating whether the content is draft. /// - /// A content is draft when it is the unpublished version of a content, which may - /// have a published version, or not. + /// + /// A content is draft when it is the unpublished version of a content, which may + /// have a published version, or not. + /// When retrieving documents from cache in non-preview mode, IsDraft is always false, + /// as only published documents are returned. When retrieving in preview mode, IsDraft can + /// either be true (document is not published, or has been edited, and what is returned + /// is the edited version) or false (document is published, and has not been edited, and + /// what is returned is the published version). + /// bool IsDraft { get; } + // fixme - consider having an IsPublished flag too + // so that when IsDraft is true, we can check whether there is a published version? + #endregion #region Tree /// - /// Gets the parent of the content. + /// Gets the parent of the content item. /// /// The parent of root content is null. IPublishedContent Parent { get; } /// - /// Gets the children of the content. + /// Gets the children of the content item. /// /// Children are sorted by their sortOrder. IEnumerable Children { get; } #endregion - - #region Properties - - /// - /// Gets a property identified by its alias. - /// - /// The property alias. - /// A value indicating whether to navigate the tree upwards until a property with a value is found. - /// The property identified by the alias. - /// - /// Navigate the tree upwards and look for a property with that alias and with a value (ie HasValue is true). - /// If found, return the property. If no property with that alias is found, having a value or not, return null. Otherwise - /// return the first property that was found with the alias but had no value (ie HasValue is false). - /// The alias is case-insensitive. - /// - IPublishedProperty GetProperty(string alias, bool recurse); - - #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs index 6cd4818587..4b579d824b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent #region PublishedElement /// - /// Gets the unique key of the published snapshot item. + /// Gets the unique key of the published element. /// Guid Key { get; } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs new file mode 100644 index 0000000000..8e1dcfd543 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -0,0 +1,37 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides a fallback strategy for getting values. + /// + // 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() to trigger fallback + + // 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). + + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + + // 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). + + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + + // 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(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(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse); + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs new file mode 100644 index 0000000000..b9c416da00 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Gives access to the current . + /// + public interface IVariationContextAccessor + { + /// + /// Gets or sets the current . + /// + VariationContext VariationContext { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs new file mode 100644 index 0000000000..b99b4ad415 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides a noop implementation for . + /// + /// + /// This is for tests etc - does not implement fallback at all. + /// + public class NoopPublishedValueFallback : IPublishedValueFallback + { + /// + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + + /// + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + + /// + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + + /// + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + + /// + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue; + + /// + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index 7f42c022a5..df3213eb07 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -18,6 +18,11 @@ namespace Umbraco.Core.Models.PublishedContent if (content == null) return null; + // in order to provide a nice, "fluent" experience, this extension method + // needs to access Current, which is not always initialized in tests - not + // very elegant, but works + if (!Current.HasContainer) return content; + // get model // if factory returns nothing, throw var model = Current.PublishedModelFactory.CreateModel(content); diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 528a545f8f..dd3e7b6d9f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -39,77 +39,101 @@ namespace Umbraco.Core.Models.PublishedContent public IPublishedContent Unwrap() => _content; #region ContentType - + + /// public virtual PublishedContentType ContentType => _content.ContentType; #endregion + + #region PublishedElement - #region Content - - public virtual int Id => _content.Id; - + /// public Guid Key => _content.Key; - public virtual int TemplateId => _content.TemplateId; + #endregion + #region PublishedContent + + /// + public virtual int Id => _content.Id; + + /// + public virtual string Name => _content.Name; + + /// + public virtual string UrlSegment => _content.UrlSegment; + + /// public virtual int SortOrder => _content.SortOrder; - public virtual string Name => _content.Name; - - public virtual IReadOnlyDictionary CultureNames => _content.CultureNames; - - public virtual string UrlName => _content.UrlName; - - public virtual string DocumentTypeAlias => _content.DocumentTypeAlias; - - public virtual int DocumentTypeId => _content.DocumentTypeId; - - public virtual string WriterName => _content.WriterName; - - public virtual string CreatorName => _content.CreatorName; - - public virtual int WriterId => _content.WriterId; - - public virtual int CreatorId => _content.CreatorId; - - public virtual string Path => _content.Path; - - public virtual DateTime CreateDate => _content.CreateDate; - - public virtual DateTime UpdateDate => _content.UpdateDate; - + /// public virtual int Level => _content.Level; + /// + public virtual string Path => _content.Path; + + /// + public virtual int TemplateId => _content.TemplateId; + + /// + public virtual int CreatorId => _content.CreatorId; + + /// + public virtual string CreatorName => _content.CreatorName; + + /// + public virtual DateTime CreateDate => _content.CreateDate; + + /// + public virtual int WriterId => _content.WriterId; + + /// + public virtual string WriterName => _content.WriterName; + + /// + public virtual DateTime UpdateDate => _content.UpdateDate; + + /// public virtual string Url => _content.Url; + /// + public virtual string GetUrl(string culture = null) => _content.GetUrl(culture); + + /// + public PublishedCultureInfos GetCulture(string culture = null) => _content.GetCulture(culture); + + /// + public IReadOnlyDictionary Cultures => _content.Cultures; + + /// public virtual PublishedItemType ItemType => _content.ItemType; + /// public virtual bool IsDraft => _content.IsDraft; #endregion #region Tree + /// public virtual IPublishedContent Parent => _content.Parent; + /// public virtual IEnumerable Children => _content.Children; #endregion #region Properties + /// public virtual IEnumerable Properties => _content.Properties; + /// public virtual IPublishedProperty GetProperty(string alias) { return _content.GetProperty(alias); } - public virtual IPublishedProperty GetProperty(string alias, bool recurse) - { - return _content.GetProperty(alias, recurse); - } - #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs new file mode 100644 index 0000000000..a93f94672a --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs @@ -0,0 +1,50 @@ +using System; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Contains culture specific values for . + /// + public class PublishedCultureInfos + { + /// + /// Initializes a new instance of the class. + /// + public PublishedCultureInfos(string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentNullOrEmptyException(nameof(culture)); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + + Culture = culture; + Name = name; + UrlSegment = name.ToUrlSegment(culture); + Date = date; + } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name of the item. + /// + public string Name { get; } + + /// + /// Gets the url segment of the item. + /// + public string UrlSegment { get; } + + /// + /// Gets the date associated with the culture. + /// + /// + /// For published culture, this is the date the culture was published. For draft + /// cultures, this is the date the culture was made available, ie the last time its + /// name changed. + /// + public DateTime Date { get; } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureName.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureName.cs deleted file mode 100644 index 59ac875aa4..0000000000 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureName.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Umbraco.Core.Models.PublishedContent -{ - /// - /// Contains the culture specific data for a item - /// - public struct PublishedCultureName - { - public PublishedCultureName(string name, string urlName) : this() - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - UrlName = urlName ?? throw new ArgumentNullException(nameof(urlName)); - } - - public string Name { get; } - public string UrlName { get; } - } -} diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index e20d8cb49c..5dc4a280e6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -20,8 +20,11 @@ namespace Umbraco.Core.Models.PublishedContent private readonly Lazy _objectValue; private readonly Lazy _xpathValue; + // RawValueProperty does not (yet?) support variants, + // only manages the current "default" value + public override object GetSourceValue(string culture = null, string segment = null) - => culture == null & segment == null ? _sourceValue : null; + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _sourceValue : null; public override bool HasValue(string culture = null, string segment = null) { @@ -30,10 +33,10 @@ namespace Umbraco.Core.Models.PublishedContent } public override object GetValue(string culture = null, string segment = null) - => culture == null & segment == null ? _objectValue.Value : null; + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _objectValue.Value : null; public override object GetXPathValue(string culture = null, string segment = null) - => culture == null & segment == null ? _xpathValue.Value : null; + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored diff --git a/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs new file mode 100644 index 0000000000..7a000c223a --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides a CurrentUICulture-based implementation of . + /// + /// + /// This accessor does not support segments. There is no need to set the current context. + /// + public class ThreadCultureVariationContextAccessor : IVariationContextAccessor + { + private readonly ConcurrentDictionary _contexts = new ConcurrentDictionary(); + + public VariationContext VariationContext + { + get => _contexts.GetOrAdd(Thread.CurrentThread.CurrentUICulture.Name, culture => new VariationContext(culture)); + set => throw new NotSupportedException(); + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/ThreadStaticVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/ThreadStaticVariationContextAccessor.cs new file mode 100644 index 0000000000..f77bb3514c --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/ThreadStaticVariationContextAccessor.cs @@ -0,0 +1,23 @@ +using System; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides a ThreadStatic-based implementation of . + /// + /// + /// Something must set the current context. + /// + public class ThreadStaticVariationContextAccessor : IVariationContextAccessor + { + [ThreadStatic] + private static VariationContext _context; + + /// + public VariationContext VariationContext + { + get => _context; + set => _context = value; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs new file mode 100644 index 0000000000..b83002fdce --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs @@ -0,0 +1,27 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents the variation context. + /// + public class VariationContext + { + /// + /// Initializes a new instance of the class. + /// + public VariationContext(string culture = null, string segment = null) + { + Culture = culture ?? ""; // cannot be null, default to invariant + Segment = segment ?? ""; // cannot be null, default to neutral + } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the segment. + /// + public string Segment { get; } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index e95e296e0a..83148d6bdb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -565,6 +565,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public static event TypedEventHandler ScopeEntityRemove; public static event TypedEventHandler ScopeVersionRemove; + // used by tests to clear events + internal static void ClearScopeEvents() + { + ScopedEntityRefresh = null; + ScopeEntityRemove = null; + ScopeVersionRemove = null; + } + protected void OnUowRefreshedEntity(ScopedEntityEventArgs args) { ScopedEntityRefresh.RaiseEvent(args, This); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 3e96f4cf0e..e723ece853 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -952,7 +952,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) foreach (var v in contentVariation) - content.SetName(v.Culture, v.Name); + content.SetCultureInfos(v.Culture, v.Name, v.Date); if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) foreach (var v in contentVariation) content.SetPublishInfos(v.Culture, v.Name, v.Date); @@ -1035,7 +1035,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.UpdateDate + Date = content.GetCultureDate(culture) }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1050,7 +1050,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetDateCulturePublished(culture) + Date = content.GetCulturePublishDate(culture) }; } @@ -1062,7 +1062,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement NodeId = content.Id, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - Edited = !content.IsCulturePublished(culture) || editedCultures.Contains(culture) // if not published, always edited + Edited = !content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture)) // if not published, always edited }; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index e7b88b3ebe..5636b79064 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -435,7 +435,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// - /// The DTO used to fetch results for a content item with it's variation info + /// The DTO used to fetch results for a content item with its variation info /// private class ContentEntityDto : BaseDto { @@ -484,6 +484,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public string Icon { get; set; } public string Thumbnail { get; set; } public bool IsContainer { get; set; } + public ContentVariation Variations { get; set; } // ReSharper restore UnusedAutoPropertyAccessor.Local // ReSharper restore UnusedMember.Local } @@ -507,8 +508,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia) sql - .AndSelect(x => NPocoSqlExtensions.Statics.Alias(x.Id, "versionId")) - .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer); + .AndSelect(x => Alias(x.Id, "versionId")) + .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); if (isContent) { @@ -617,7 +618,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia) sql .AndBy(x => x.Id) - .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer); + .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); if (sort) sql.OrderBy(x => x.SortOrder); @@ -868,6 +869,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private static void BuildDocumentEntity(DocumentEntitySlim entity, BaseDto dto) { BuildContentEntity(entity, dto); + entity.AdditionalData[EntitySlim.AdditionalVariations] = dto.Variations; } private static EntitySlim BuildContentEntity(BaseDto dto) @@ -901,14 +903,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // EntitySlim does not track changes var entity = new DocumentEntitySlim(); BuildDocumentEntity(entity, dto); - + //fixme we need to set these statuses for each variant, see notes in IDocumentEntitySlim entity.Edited = dto.Edited; entity.Published = dto.Published; - var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - if (dto.VariationInfo != null) + if (dto.Variations.Has(ContentVariation.CultureNeutral) && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { + var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var info in dto.VariationInfo) { var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); @@ -916,6 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement variantInfo[isoCode] = info.Name; } entity.CultureNames = variantInfo; + entity.AdditionalData[EntitySlim.AdditionalCultureNames] = variantInfo; } return entity; } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index a3db23e5ed..f02441eba0 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -22,6 +22,12 @@ namespace Umbraco.Core.Services.Implement // that one is always immediate (transactional) public static event TypedEventHandler.EventArgs> UowRefreshedEntity; + // used by tests to clear events + internal static void ClearScopeEvents() + { + UowRefreshedEntity = null; + } + // these must be dispatched public static event TypedEventHandler> Saving; public static event TypedEventHandler> Saved; diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 4369e8faac..98e831f73f 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -16,6 +16,7 @@ using System.Web.Security; using Umbraco.Core.Strings; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Composing; +using Umbraco.Core.Exceptions; using Umbraco.Core.IO; namespace Umbraco.Core @@ -1069,7 +1070,7 @@ namespace Umbraco.Core /// The replacement character. /// The filtered string. public static string ReplaceMany(this string text, char[] chars, char replacement) - { + { if (text == null) throw new ArgumentNullException(nameof(text)); if (chars == null) throw new ArgumentNullException(nameof(chars)); @@ -1078,7 +1079,7 @@ namespace Umbraco.Core text = text.Replace(chars[i], replacement); return text; - } + } // FORMAT STRINGS @@ -1111,7 +1112,7 @@ namespace Umbraco.Core /// The text to filter. /// The culture. /// The safe alias. - public static string ToSafeAlias(this string alias, CultureInfo culture) + public static string ToSafeAlias(this string alias, string culture) { return Current.ShortStringHelper.CleanStringForSafeAlias(alias, culture); } @@ -1134,7 +1135,7 @@ namespace Umbraco.Core /// The culture. /// The safe alias. /// Checks UmbracoSettings.ForceSafeAliases to determine whether it should filter the text. - public static string ToSafeAliasWithForcingCheck(this string alias, CultureInfo culture) + public static string ToSafeAliasWithForcingCheck(this string alias, string culture) { return UmbracoConfig.For.UmbracoSettings().Content.ForceSafeAliases ? alias.ToSafeAlias(culture) : alias; } @@ -1147,8 +1148,9 @@ namespace Umbraco.Core /// The text to filter. /// The safe url segment. public static string ToUrlSegment(this string text) - { - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("message", nameof(text)); + { + if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); + return Current.ShortStringHelper.CleanStringForUrlSegment(text); } @@ -1158,11 +1160,10 @@ namespace Umbraco.Core /// The text to filter. /// The culture. /// The safe url segment. - public static string ToUrlSegment(this string text, CultureInfo culture) - { - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("message", nameof(text)); - if (culture == null) throw new ArgumentNullException(nameof(culture)); - + public static string ToUrlSegment(this string text, string culture) + { + if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); + return Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); } @@ -1203,7 +1204,7 @@ namespace Umbraco.Core /// strings are cleaned up to camelCase and Ascii. /// The culture. /// The clean string. - public static string ToCleanString(this string text, CleanStringType stringType, CultureInfo culture) + public static string ToCleanString(this string text, CleanStringType stringType, string culture) { return Current.ShortStringHelper.CleanString(text, stringType, culture); } @@ -1217,7 +1218,7 @@ namespace Umbraco.Core /// The separator. /// The culture. /// The clean string. - public static string ToCleanString(this string text, CleanStringType stringType, char separator, CultureInfo culture) + public static string ToCleanString(this string text, CleanStringType stringType, char separator, string culture) { return Current.ShortStringHelper.CleanString(text, stringType, separator, culture); } @@ -1263,7 +1264,7 @@ namespace Umbraco.Core /// The text to filter. /// The culture. /// The safe filename. - public static string ToSafeFileName(this string text, CultureInfo culture) + public static string ToSafeFileName(this string text, string culture) { return Current.ShortStringHelper.CleanStringForSafeFileName(text, culture); } diff --git a/src/Umbraco.Core/Strings/ContentBaseExtensions.cs b/src/Umbraco.Core/Strings/ContentBaseExtensions.cs index 29ef235f2b..c63b546cdc 100644 --- a/src/Umbraco.Core/Strings/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Strings/ContentBaseExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core.Models; @@ -11,23 +10,6 @@ namespace Umbraco.Core.Strings /// internal static class ContentBaseExtensions { - - /// - /// Gets the default url segment for a specified content. - /// - /// The content. - /// - /// The url segment. - public static string GetUrlSegment(this IContentBase content, IEnumerable urlSegmentProviders) - { - if (content == null) throw new ArgumentNullException("content"); - if (urlSegmentProviders == null) throw new ArgumentNullException("urlSegmentProviders"); - - var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content)).FirstOrDefault(u => u != null); - url = url ?? new DefaultUrlSegmentProvider().GetUrlSegment(content); // be safe - return url; - } - /// /// Gets the url segment for a specified content and culture. /// @@ -35,11 +17,10 @@ namespace Umbraco.Core.Strings /// The culture. /// /// The url segment. - public static string GetUrlSegment(this IContentBase content, CultureInfo culture, IEnumerable urlSegmentProviders) + public static string GetUrlSegment(this IContentBase content, IEnumerable urlSegmentProviders, string culture = null) { - if (content == null) throw new ArgumentNullException("content"); - if (culture == null) throw new ArgumentNullException("culture"); - if (urlSegmentProviders == null) throw new ArgumentNullException("urlSegmentProviders"); + if (content == null) throw new ArgumentNullException(nameof(content)); + if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); url = url ?? new DefaultUrlSegmentProvider().GetUrlSegment(content, culture); // be safe diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index 3a9d9433f4..eb06d0279a 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -18,7 +17,7 @@ namespace Umbraco.Core.Strings /// public class DefaultShortStringHelper : IShortStringHelper { - #region Ctor and vars + #region Ctor, consts and vars public DefaultShortStringHelper(IUmbracoSettingsSection settings) { @@ -138,7 +137,7 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// /// Safe aliases are Ascii only. /// - public virtual string CleanStringForSafeAlias(string text, CultureInfo culture) + public virtual string CleanStringForSafeAlias(string text, string culture) { return CleanString(text, CleanStringType.Alias, culture); } @@ -166,7 +165,7 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// /// Url segments are Ascii only (no accents...). /// - public virtual string CleanStringForUrlSegment(string text, CultureInfo culture) + public virtual string CleanStringForUrlSegment(string text, string culture) { return CleanString(text, CleanStringType.UrlSegment, culture); } @@ -190,11 +189,12 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// The text to filter. /// The culture. /// The safe filename. - public virtual string CleanStringForSafeFileName(string text, CultureInfo culture) + public virtual string CleanStringForSafeFileName(string text, string culture) { if (string.IsNullOrWhiteSpace(text)) return string.Empty; + culture = culture ?? ""; text = text.ReplaceMany(Path.GetInvalidFileNameChars(), '-'); var name = Path.GetFileNameWithoutExtension(text); @@ -266,7 +266,7 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// strings are cleaned up to camelCase and Ascii. /// The culture. /// The clean string. - public string CleanString(string text, CleanStringType stringType, CultureInfo culture) + public string CleanString(string text, CleanStringType stringType, string culture) { return CleanString(text, stringType, culture, null); } @@ -280,16 +280,16 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// The separator. /// The culture. /// The clean string. - public string CleanString(string text, CleanStringType stringType, char separator, CultureInfo culture) + public string CleanString(string text, CleanStringType stringType, char separator, string culture) { return CleanString(text, stringType, culture, separator); } - protected virtual string CleanString(string text, CleanStringType stringType, CultureInfo culture, char? separator) + protected virtual string CleanString(string text, CleanStringType stringType, string culture, char? separator) { // be safe if (text == null) throw new ArgumentNullException(nameof(text)); - if (culture == null) throw new ArgumentNullException(nameof(culture)); + culture = culture ?? ""; // get config var config = _config.For(stringType, culture); @@ -367,11 +367,12 @@ function validateSafeAlias(input, value, immediate, callback) {{ // that the utf8 version. Micro-optimizing sometimes isn't such a good idea. // note: does NOT support surrogate pairs in text - internal string CleanCodeString(string text, CleanStringType caseType, char separator, CultureInfo culture, DefaultShortStringHelperConfig.Config config) + internal string CleanCodeString(string text, CleanStringType caseType, char separator, string culture, DefaultShortStringHelperConfig.Config config) { int opos = 0, ipos = 0; var state = StateBreak; + culture = culture ?? ""; caseType &= CleanStringType.CaseMask; // if we apply global ToUpper or ToLower to text here @@ -501,9 +502,10 @@ function validateSafeAlias(input, value, immediate, callback) {{ // note: supports surrogate pairs in input string internal void CopyTerm(string input, int ipos, char[] output, ref int opos, int len, - CleanStringType caseType, CultureInfo culture, bool isAcronym) + CleanStringType caseType, string culture, bool isAcronym) { var term = input.Substring(ipos, len); + var cultureInfo = string.IsNullOrEmpty(culture) ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); if (isAcronym) { @@ -529,13 +531,13 @@ function validateSafeAlias(input, value, immediate, callback) {{ break; case CleanStringType.LowerCase: - term = term.ToLower(culture); + term = term.ToLower(cultureInfo); term.CopyTo(0, output, opos, term.Length); opos += term.Length; break; case CleanStringType.UpperCase: - term = term.ToUpper(culture); + term = term.ToUpper(cultureInfo); term.CopyTo(0, output, opos, term.Length); opos += term.Length; break; @@ -546,18 +548,18 @@ function validateSafeAlias(input, value, immediate, callback) {{ if (char.IsSurrogate(c)) { s = term.Substring(ipos, 2); - s = opos == 0 ? s.ToLower(culture) : s.ToUpper(culture); + s = opos == 0 ? s.ToLower(cultureInfo) : s.ToUpper(cultureInfo); s.CopyTo(0, output, opos, s.Length); opos += s.Length; i++; // surrogate pair len is 2 } else { - output[opos] = opos++ == 0 ? char.ToLower(c, culture) : char.ToUpper(c, culture); + output[opos] = opos++ == 0 ? char.ToLower(c, cultureInfo) : char.ToUpper(c, cultureInfo); } if (len > i) { - term = term.Substring(i).ToLower(culture); + term = term.Substring(i).ToLower(cultureInfo); term.CopyTo(0, output, opos, term.Length); opos += term.Length; } @@ -569,18 +571,18 @@ function validateSafeAlias(input, value, immediate, callback) {{ if (char.IsSurrogate(c)) { s = term.Substring(ipos, 2); - s = s.ToUpper(culture); + s = s.ToUpper(cultureInfo); s.CopyTo(0, output, opos, s.Length); opos += s.Length; i++; // surrogate pair len is 2 } else { - output[opos++] = char.ToUpper(c, culture); + output[opos++] = char.ToUpper(c, cultureInfo); } if (len > i) { - term = term.Substring(i).ToLower(culture); + term = term.Substring(i).ToLower(cultureInfo); term.CopyTo(0, output, opos, term.Length); opos += term.Length; } @@ -592,14 +594,14 @@ function validateSafeAlias(input, value, immediate, callback) {{ if (char.IsSurrogate(c)) { s = term.Substring(ipos, 2); - s = opos == 0 ? s : s.ToUpper(culture); + s = opos == 0 ? s : s.ToUpper(cultureInfo); s.CopyTo(0, output, opos, s.Length); opos += s.Length; i++; // surrogate pair len is 2 } else { - output[opos] = opos++ == 0 ? c : char.ToUpper(c, culture); + output[opos] = opos++ == 0 ? c : char.ToUpper(c, cultureInfo); } if (len > i) { @@ -668,8 +670,6 @@ function validateSafeAlias(input, value, immediate, callback) {{ return new string(output, 0, opos); } - #endregion - - + #endregion } } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index 8a6aca632b..2bbade0fd8 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core.Configuration.UmbracoSettings; @@ -8,7 +7,7 @@ namespace Umbraco.Core.Strings { public class DefaultShortStringHelperConfig { - private readonly Dictionary> _configs = new Dictionary>(); + private readonly Dictionary> _configs = new Dictionary>(); public DefaultShortStringHelperConfig Clone() { @@ -29,7 +28,7 @@ namespace Umbraco.Core.Strings return config; } - public CultureInfo DefaultCulture { get; set; } = CultureInfo.InvariantCulture; + public string DefaultCulture { get; set; } = ""; // invariant public Dictionary UrlReplaceCharacters { get; set; } @@ -45,10 +44,12 @@ namespace Umbraco.Core.Strings return WithConfig(DefaultCulture, stringRole, config); } - public DefaultShortStringHelperConfig WithConfig(CultureInfo culture, CleanStringType stringRole, Config config) + public DefaultShortStringHelperConfig WithConfig(string culture, CleanStringType stringRole, Config config) { if (config == null) throw new ArgumentNullException(nameof(config)); + culture = culture ?? ""; + if (_configs.ContainsKey(culture) == false) _configs[culture] = new Dictionary(); _configs[culture][stringRole] = config; @@ -112,8 +113,9 @@ namespace Umbraco.Core.Strings // internal: we don't want ppl to retrieve a config and modify it // (the helper uses a private clone to prevent modifications) - internal Config For(CleanStringType stringType, CultureInfo culture) + internal Config For(CleanStringType stringType, string culture) { + culture = culture ?? ""; stringType = stringType & CleanStringType.RoleMask; Dictionary config; diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index d2e47e04b4..bc52e94f7b 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using Umbraco.Core.Models; +using Umbraco.Core.Models; namespace Umbraco.Core.Strings { @@ -8,34 +7,24 @@ namespace Umbraco.Core.Strings /// public class DefaultUrlSegmentProvider : IUrlSegmentProvider { - /// - /// Gets the default url segment for a specified content. - /// - /// The content. - /// The url segment. - public string GetUrlSegment(IContentBase content) - { - return GetUrlSegmentSource(content).ToUrlSegment(); - } - /// /// Gets the url segment for a specified content and culture. /// /// The content. /// The culture. /// The url segment. - public string GetUrlSegment(IContentBase content, CultureInfo culture) + public string GetUrlSegment(IContentBase content, string culture = null) { - return GetUrlSegmentSource(content).ToUrlSegment(culture); + return GetUrlSegmentSource(content, culture).ToUrlSegment(culture); } - private static string GetUrlSegmentSource(IContentBase content) + private static string GetUrlSegmentSource(IContentBase content, string culture) { string source = null; if (content.HasProperty(Constants.Conventions.Content.UrlName)) - source = (content.GetValue(Constants.Conventions.Content.UrlName) ?? string.Empty).Trim(); + source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(source)) - source = content.Name; + source = content.GetName(culture); return source; } } diff --git a/src/Umbraco.Core/Strings/IShortStringHelper.cs b/src/Umbraco.Core/Strings/IShortStringHelper.cs index 7232b0efe7..afe2166330 100644 --- a/src/Umbraco.Core/Strings/IShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/IShortStringHelper.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Globalization; - -namespace Umbraco.Core.Strings +namespace Umbraco.Core.Strings { /// /// Provides string functions for short strings such as aliases or url segments. @@ -31,7 +28,7 @@ namespace Umbraco.Core.Strings /// The text to filter. /// The culture. /// The safe alias. - string CleanStringForSafeAlias(string text, CultureInfo culture); + string CleanStringForSafeAlias(string text, string culture); /// /// Cleans a string to produce a string that can safely be used in an url segment. @@ -47,7 +44,7 @@ namespace Umbraco.Core.Strings /// The text to filter. /// The culture. /// The safe url segment. - string CleanStringForUrlSegment(string text, CultureInfo culture); + string CleanStringForUrlSegment(string text, string culture); /// /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, @@ -66,7 +63,7 @@ namespace Umbraco.Core.Strings /// The culture. /// The safe filename. /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - string CleanStringForSafeFileName(string text, CultureInfo culture); + string CleanStringForSafeFileName(string text, string culture); /// /// Splits a pascal-cased string by inserting a separator in between each term. @@ -106,7 +103,7 @@ namespace Umbraco.Core.Strings /// strings are cleaned up to camelCase and Ascii. /// The culture. /// The clean string. - string CleanString(string text, CleanStringType stringType, CultureInfo culture); + string CleanString(string text, CleanStringType stringType, string culture); /// /// Cleans a string in the context of a specified culture, using a specified separator. @@ -117,6 +114,6 @@ namespace Umbraco.Core.Strings /// The separator. /// The culture. /// The clean string. - string CleanString(string text, CleanStringType stringType, char separator, CultureInfo culture); + string CleanString(string text, CleanStringType stringType, char separator, string culture); } } diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index 824ae50aa4..1acbcea769 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -9,13 +9,6 @@ namespace Umbraco.Core.Strings /// Url segments should comply with IETF RFCs regarding content, encoding, etc. public interface IUrlSegmentProvider { - /// - /// Gets the default url segment for a specified content. - /// - /// The content. - /// The url segment. - string GetUrlSegment(IContentBase content); - /// /// Gets the url segment for a specified content and culture. /// @@ -25,7 +18,7 @@ namespace Umbraco.Core.Strings /// This is for when Umbraco is capable of managing more than one url /// per content, in 1-to-1 multilingual configurations. Then there would be one /// url per culture. - string GetUrlSegment(IContentBase content, CultureInfo culture); + string GetUrlSegment(IContentBase content, string culture = null); //TODO: For the 301 tracking, we need to add another extended interface to this so that // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e39b6ed9c8..c476838302 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -355,7 +355,13 @@ - + + + + + + + diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 742e71ad93..0c240336b7 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -322,5 +322,16 @@ namespace Umbraco.Core { return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); } + + /// + /// Replaces the host of a uri. + /// + /// The uri. + /// A replacement host. + /// The same uri, with its host replaced. + public static Uri ReplaceHost(this Uri uri, string host) + { + return new UriBuilder(uri) { Host = host }.Uri; + } } } diff --git a/src/Umbraco.Examine/UmbracoContentIndexer.cs b/src/Umbraco.Examine/UmbracoContentIndexer.cs index 0c02ce02b9..487d238c25 100644 --- a/src/Umbraco.Examine/UmbracoContentIndexer.cs +++ b/src/Umbraco.Examine/UmbracoContentIndexer.cs @@ -321,7 +321,7 @@ namespace Umbraco.Examine { foreach (var c in content) { - var urlValue = c.GetUrlSegment(urlSegmentProviders); + var urlValue = c.GetUrlSegment(urlSegmentProviders); // for now, index with invariant culture var values = new Dictionary { {"icon", new object[] {c.ContentType.Icon}}, @@ -348,7 +348,7 @@ namespace Umbraco.Examine { //only add the value if its not null or empty (we'll check for string explicitly here too) //fixme support variants with language id - var val = property.GetValue(); + var val = property.GetValue("", ""); // for now, index the invariant values switch (val) { case null: diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 8212243e26..bf70922768 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -62,7 +63,7 @@ namespace Umbraco.Tests.Cache.PublishedCache _xml.LoadXml(GetXml()); var xmlStore = new XmlStore(() => _xml, null, null, null); var cacheProvider = new StaticCacheProvider(); - var domainCache = new DomainCache(ServiceContext.DomainService, SystemDefaultCultureProvider); + var domainCache = new DomainCache(ServiceContext.DomainService, DefaultCultureAccessor); var publishedShapshot = new Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedSnapshot( new PublishedContentCache(xmlStore, domainCache, cacheProvider, globalSettings, new SiteDomainHelper(), ContentTypesCache, null, null), new PublishedMediaCache(xmlStore, ServiceContext.MediaService, ServiceContext.UserService, cacheProvider, ContentTypesCache), @@ -78,7 +79,7 @@ namespace Umbraco.Tests.Cache.PublishedCache umbracoSettings, Enumerable.Empty(), globalSettings, - ServiceContext.EntityService); + new TestVariationContextAccessor()); _cache = _umbracoContext.ContentCache; } diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 34def77faa..64194ebb47 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -16,6 +16,7 @@ using Current = Umbraco.Web.Composing.Current; using LightInject; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Tests.PublishedContent; namespace Umbraco.Tests.Cache.PublishedCache { @@ -23,6 +24,8 @@ namespace Umbraco.Tests.Cache.PublishedCache [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class PublishMediaCacheTests : BaseWebTest { + private Dictionary _mediaTypes; + protected override void Compose() { base.Compose(); @@ -32,6 +35,21 @@ namespace Umbraco.Tests.Cache.PublishedCache .Append(); } + protected override void Initialize() + { + base.Initialize(); + var type = new AutoPublishedContentType(22, "myType", new PublishedPropertyType[] { }); + var image = new AutoPublishedContentType(23, "Image", new PublishedPropertyType[] { }); + var testMediaType = new AutoPublishedContentType(24, "TestMediaType", new PublishedPropertyType[] { }); + _mediaTypes = new Dictionary + { + { type.Alias, type }, + { image.Alias, image }, + { testMediaType.Alias, testMediaType } + }; + ContentTypesCache.GetPublishedContentTypeByAlias = alias => _mediaTypes[alias]; + } + private IMediaType MakeNewMediaType(IUser user, string text, int parentId = -1) { var mt = new MediaType(parentId) { Name = text, Alias = text, Thumbnail = "icon-folder", Icon = "icon-folder" }; @@ -69,6 +87,7 @@ namespace Umbraco.Tests.Cache.PublishedCache { var user = ServiceContext.UserService.GetUserById(0); var mType = MakeNewMediaType(user, "TestMediaType"); + _mediaTypes[mType.Alias] = new PublishedContentType(mType, null); var mRoot = MakeNewMedia("MediaRoot", mType, user, -1); var mChild1 = MakeNewMedia("Child1", mType, user, mRoot.Id); @@ -82,8 +101,8 @@ namespace Umbraco.Tests.Cache.PublishedCache Assert.AreEqual(mRoot.CreateDate.ToString("dd/MM/yyyy HH:mm:ss"), publishedMedia.CreateDate.ToString("dd/MM/yyyy HH:mm:ss")); Assert.AreEqual(mRoot.CreatorId, publishedMedia.CreatorId); //Assert.AreEqual(mRoot.User.Name, publishedMedia.CreatorName); - Assert.AreEqual(mRoot.ContentType.Alias, publishedMedia.DocumentTypeAlias); - Assert.AreEqual(mRoot.ContentType.Id, publishedMedia.DocumentTypeId); + Assert.AreEqual(mRoot.ContentType.Alias, publishedMedia.ContentType.Alias); + Assert.AreEqual(mRoot.ContentType.Id, publishedMedia.ContentType.Id); Assert.AreEqual(mRoot.Level, publishedMedia.Level); Assert.AreEqual(mRoot.Name, publishedMedia.Name); Assert.AreEqual(mRoot.Path, publishedMedia.Path); @@ -186,11 +205,11 @@ namespace Umbraco.Tests.Cache.PublishedCache }; var result = new SearchResult("1234", 1, 1, () => fields.ToDictionary(x => x.Key, x => new List { x.Value })); - + var store = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null), ServiceContext.MediaService, ServiceContext.UserService, new StaticCacheProvider(), ContentTypesCache); var doc = store.CreateFromCacheValues(store.ConvertFromSearchResult(result)); - DoAssert(doc, 1234, key, 0, 0, "/media/test.jpg", "Image", 0, "Shannon", "Shannon", 0, 0, "-1,1234", DateTime.Parse("2012-07-17T10:34:09"), DateTime.Parse("2012-07-16T10:34:09"), 2); + DoAssert(doc, 1234, key, 0, 0, "/media/test.jpg", "Image", 23, "Shannon", "Shannon", 0, 0, "-1,1234", DateTime.Parse("2012-07-17T10:34:09"), DateTime.Parse("2012-07-16T10:34:09"), 2); Assert.AreEqual(null, doc.Parent); } @@ -206,7 +225,7 @@ namespace Umbraco.Tests.Cache.PublishedCache var cache = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null), ServiceContext.MediaService, ServiceContext.UserService, new StaticCacheProvider(), ContentTypesCache); var doc = cache.CreateFromCacheValues(cache.ConvertFromXPathNavigator(navigator, true)); - DoAssert(doc, 2000, key, 0, 2, "image1", "Image", 2044, "Shannon", "Shannon", 33, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1); + DoAssert(doc, 2000, key, 0, 2, "image1", "Image", 23, "Shannon", "Shannon", 33, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1); Assert.AreEqual(null, doc.Parent); Assert.AreEqual(2, doc.Children.Count()); Assert.AreEqual(2001, doc.Children.ElementAt(0).Id); @@ -271,7 +290,7 @@ namespace Umbraco.Tests.Cache.PublishedCache }; } - private PublishedMediaCache.DictionaryPublishedContent GetDictionaryDocument( + private DictionaryPublishedContent GetDictionaryDocument( string idKey = "id", string templateKey = "template", string nodeNameKey = "nodeName", @@ -284,11 +303,11 @@ namespace Umbraco.Tests.Cache.PublishedCache { if (children == null) children = new List(); - var dicDoc = new PublishedMediaCache.DictionaryPublishedContent( + var dicDoc = new DictionaryPublishedContent( //the dictionary GetDictionary(idVal, keyVal, parentIdVal, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey), //callback to get the parent - d => new PublishedMediaCache.DictionaryPublishedContent( + d => new DictionaryPublishedContent( // the dictionary GetDictionary(parentIdVal, default(Guid), -1, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey), // callback to get the parent: there is no parent @@ -317,7 +336,7 @@ namespace Umbraco.Tests.Cache.PublishedCache } private void DoAssert( - PublishedMediaCache.DictionaryPublishedContent dicDoc, + DictionaryPublishedContent dicDoc, int idVal = 1234, Guid keyVal = default(Guid), int templateIdVal = 0, @@ -374,9 +393,9 @@ namespace Umbraco.Tests.Cache.PublishedCache Assert.AreEqual(keyVal, doc.Key); Assert.AreEqual(templateIdVal, doc.TemplateId); Assert.AreEqual(sortOrderVal, doc.SortOrder); - Assert.AreEqual(urlNameVal, doc.UrlName); - Assert.AreEqual(nodeTypeAliasVal, doc.DocumentTypeAlias); - Assert.AreEqual(nodeTypeIdVal, doc.DocumentTypeId); + Assert.AreEqual(urlNameVal, doc.UrlSegment); + Assert.AreEqual(nodeTypeAliasVal, doc.ContentType.Alias); + Assert.AreEqual(nodeTypeIdVal, doc.ContentType.Id); Assert.AreEqual(writerNameVal, doc.WriterName); Assert.AreEqual(creatorNameVal, doc.CreatorName); Assert.AreEqual(writerIdVal, doc.WriterId); diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index 246626f0a8..73f95c44a7 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Integration { base.TearDown(); - _h1.Unbind(); + _h1?.Unbind(); // clear ALL events diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index bc0891e397..1b9000617e 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Models const string langUk = "en-UK"; // throws if the content type does not support the variation - Assert.Throws(() => content.SetName(langFr, "name-fr")); + Assert.Throws(() => content.SetName("name-fr", langFr)); // now it will work contentType.Variations = ContentVariation.CultureNeutral; @@ -173,13 +173,13 @@ namespace Umbraco.Tests.Models // invariant name works content.Name = "name"; Assert.AreEqual("name", content.GetName(null)); - content.SetName(null, "name2"); + content.SetName("name2", null); Assert.AreEqual("name2", content.Name); Assert.AreEqual("name2", content.GetName(null)); // variant names work - content.SetName(langFr, "name-fr"); - content.SetName(langUk, "name-uk"); + content.SetName("name-fr", langFr); + content.SetName("name-uk", langUk); Assert.AreEqual("name-fr", content.GetName(langFr)); Assert.AreEqual("name-uk", content.GetName(langUk)); @@ -245,7 +245,7 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values Assert.IsFalse(content.TryPublishValues(langFr)); // no name - content.SetName(langFr, "name-fr"); + content.SetName("name-fr", langFr); content.TryPublishValues(langFr); Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); @@ -326,29 +326,29 @@ namespace Umbraco.Tests.Models // works with a name // and then FR is available, and published - content.SetName(langFr, "name-fr"); + content.SetName("name-fr", langFr); content.TryPublishValues(langFr); // now UK is available too - content.SetName(langUk, "name-uk"); + content.SetName("name-uk", langUk); // test available, published Assert.IsTrue(content.IsCultureAvailable(langFr)); Assert.IsTrue(content.IsCulturePublished(langFr)); Assert.AreEqual("name-fr", content.GetPublishName(langFr)); - Assert.AreNotEqual(DateTime.MinValue, content.GetDateCulturePublished(langFr)); + Assert.AreNotEqual(DateTime.MinValue, content.GetCulturePublishDate(langFr)); Assert.IsFalse(content.IsCultureEdited(langFr)); // once published, edited is *wrong* until saved Assert.IsTrue(content.IsCultureAvailable(langUk)); Assert.IsFalse(content.IsCulturePublished(langUk)); Assert.IsNull(content.GetPublishName(langUk)); - Assert.Throws(() => content.GetDateCulturePublished(langUk)); // not published! + Assert.Throws(() => content.GetCulturePublishDate(langUk)); // not published! Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited Assert.IsFalse(content.IsCultureAvailable(langEs)); Assert.IsFalse(content.IsCulturePublished(langEs)); Assert.IsNull(content.GetPublishName(langEs)); - Assert.Throws(() => content.GetDateCulturePublished(langEs)); // not published! + Assert.Throws(() => content.GetCulturePublishDate(langEs)); // not published! Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited // cannot test IsCultureEdited here - as that requires the content service and repository diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 035c857e35..9675707439 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -685,40 +685,40 @@ namespace Umbraco.Tests.Persistence.Repositories { //2x content types, one invariant, one variant - var invariantCT = MockedContentTypes.CreateSimpleContentType("umbInvariantTextpage", "Invariant Textpage"); - invariantCT.Variations = ContentVariation.InvariantNeutral; - foreach (var p in invariantCT.PropertyTypes) p.Variations = ContentVariation.InvariantNeutral; - ServiceContext.FileService.SaveTemplate(invariantCT.DefaultTemplate); // else, FK violation on contentType! - ServiceContext.ContentTypeService.Save(invariantCT); + var invariantCt = MockedContentTypes.CreateSimpleContentType("umbInvariantTextpage", "Invariant Textpage"); + invariantCt.Variations = ContentVariation.InvariantNeutral; + foreach (var p in invariantCt.PropertyTypes) p.Variations = ContentVariation.InvariantNeutral; + ServiceContext.FileService.SaveTemplate(invariantCt.DefaultTemplate); // else, FK violation on contentType! + ServiceContext.ContentTypeService.Save(invariantCt); - var variantCT = MockedContentTypes.CreateSimpleContentType("umbVariantTextpage", "Variant Textpage"); - variantCT.Variations = ContentVariation.CultureNeutral; - var propTypes = variantCT.PropertyTypes.ToList(); - for (int i = 0; i < propTypes.Count; i++) + var variantCt = MockedContentTypes.CreateSimpleContentType("umbVariantTextpage", "Variant Textpage"); + variantCt.Variations = ContentVariation.CultureNeutral; + var propTypes = variantCt.PropertyTypes.ToList(); + for (var i = 0; i < propTypes.Count; i++) { var p = propTypes[i]; //every 2nd one is variant p.Variations = i % 2 == 0 ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; } - ServiceContext.FileService.SaveTemplate(variantCT.DefaultTemplate); // else, FK violation on contentType! - ServiceContext.ContentTypeService.Save(variantCT); + ServiceContext.FileService.SaveTemplate(variantCt.DefaultTemplate); // else, FK violation on contentType! + ServiceContext.ContentTypeService.Save(variantCt); - invariantCT.AllowedContentTypes = new[] { new ContentTypeSort(invariantCT.Id, 0), new ContentTypeSort(variantCT.Id, 1) }; - ServiceContext.ContentTypeService.Save(invariantCT); + invariantCt.AllowedContentTypes = new[] { new ContentTypeSort(invariantCt.Id, 0), new ContentTypeSort(variantCt.Id, 1) }; + ServiceContext.ContentTypeService.Save(invariantCt); //create content - var root = MockedContent.CreateSimpleContent(invariantCT); + var root = MockedContent.CreateSimpleContent(invariantCt); ServiceContext.ContentService.Save(root); - for (int i = 0; i < 25; i++) + for (var i = 0; i < 25; i++) { var isInvariant = i % 2 == 0; - var name = (isInvariant ? "INV" : "VAR") + "_" + Guid.NewGuid().ToString(); + var name = (isInvariant ? "INV" : "VAR") + "_" + Guid.NewGuid(); var culture = isInvariant ? null : "en-US"; var child = MockedContent.CreateSimpleContent( - isInvariant ? invariantCT : variantCT, + isInvariant ? invariantCt : variantCt, name, root, culture, setPropertyValues: isInvariant); @@ -751,9 +751,9 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(25, totalRecords); foreach (var r in result) { - var isInvariant = r.ContentType.Alias == "umbInvariantTextpage"; + var isInvariant = r.ContentType.Alias == "umbInvariantTextpage"; var name = isInvariant ? r.Name : r.Names["en-US"]; - var namePrefix = (isInvariant ? "INV" : "VAR"); + var namePrefix = isInvariant ? "INV" : "VAR"; //ensure the correct name (invariant vs variant) is in the result Assert.IsTrue(name.StartsWith(namePrefix)); diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index c806930704..0bc4fb9de4 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -273,10 +273,9 @@ namespace Umbraco.Tests.Published public override int TemplateId { get; } public override int SortOrder { get; } public override string Name { get; } - public override IReadOnlyDictionary CultureNames => throw new NotSupportedException(); - public override string UrlName { get; } - public override string DocumentTypeAlias { get; } - public override int DocumentTypeId { get; } + public override PublishedCultureInfos GetCulture(string culture = ".") => throw new NotSupportedException(); + public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); + public override string UrlSegment { get; } public override string WriterName { get; } public override string CreatorName { get; } public override int WriterId { get; } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs new file mode 100644 index 0000000000..2703cd448d --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web; +using Umbraco.Web.Cache; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + public class NuCacheTests + { + [Test] + public void StandaloneVariations() + { + // this test implements a full standalone NuCache (based upon a test IDataSource, does not + // use any local db files, does not rely on any database) - and tests variations + + SettingsForTests.ConfigureSettings(SettingsForTests.GenerateMockUmbracoSettings()); + var globalSettings = UmbracoConfig.For.GlobalSettings(); + + // create a content node kit + var kit = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(1, Guid.NewGuid(), 0, "-1,1", 0, -1, DateTime.Now, 0), + DraftData = new ContentData { Name="It Works2!", Published = false, TemplateId = 0, VersionId = 2, VersionDate = DateTime.Now, WriterId = 0, + Properties = new Dictionary { { "prop", new[] + { + new PropertyData { Culture = "", Segment = "", Value = "val2" }, + new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr2" }, + new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk2" } + } } }, + CultureInfos = new Dictionary + { + { "fr-FR", new CultureVariation { Name = "name-fr2", Date = new DateTime(2018, 01, 03, 01, 00, 00) } }, + { "en-UK", new CultureVariation { Name = "name-uk2", Date = new DateTime(2018, 01, 04, 01, 00, 00) } } + } + }, + PublishedData = new ContentData { Name="It Works1!", Published = true, TemplateId = 0, VersionId = 1, VersionDate = DateTime.Now, WriterId = 0, + Properties = new Dictionary { { "prop", new[] + { + new PropertyData { Culture = "", Segment = "", Value = "val1" }, + new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr1" }, + new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk1" } + } } }, + CultureInfos = new Dictionary + { + { "fr-FR", new CultureVariation { Name = "name-fr1", Date = new DateTime(2018, 01, 01, 01, 00, 00) } }, + { "en-UK", new CultureVariation { Name = "name-uk1", Date = new DateTime(2018, 01, 02, 01, 00, 00) } } + } + } + }; + + // create a data source for NuCache + var dataSource = new TestDataSource(kit); + + var runtime = Mock.Of(); + Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); + + // create data types, property types and content types + var dataType = new DataType(new VoidEditor("Editor", Mock.Of())) { Id = 3 }; + + var dataTypes = new[] + { + dataType + }; + + var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; + var contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; + contentType.AddPropertyType(propertyType); + + var contentTypes = new[] + { + contentType + }; + + var contentTypeService = Mock.Of(); + Mock.Get(contentTypeService).Setup(x => x.GetAll()).Returns(contentTypes); + Mock.Get(contentTypeService).Setup(x => x.GetAll(It.IsAny())).Returns(contentTypes); + + var dataTypeService = Mock.Of(); + Mock.Get(dataTypeService).Setup(x => x.GetAll()).Returns(dataTypes); + + // create a service context + var serviceContext = new ServiceContext( + dataTypeService : dataTypeService, + memberTypeService: Mock.Of(), + memberService: Mock.Of(), + contentTypeService : contentTypeService, + localizationService: Mock.Of() + ); + + // create a scope provider + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + + // create a published content type factory + var contentTypeFactory = new PublishedContentTypeFactory( + Mock.Of(), + new PropertyValueConverterCollection(Array.Empty()), + dataTypeService); + + // create a variation accessor + var variationAccessor = new TestVariationContextAccessor(); + + // at last, create the complete NuCache snapshot service! + var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var snapshotService = new PublishedSnapshotService(options, + null, + runtime, + serviceContext, + contentTypeFactory, + null, + new TestPublishedSnapshotAccessor(), + variationAccessor, + Mock.Of(), + scopeProvider, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestDefaultCultureAccessor(), + dataSource, + globalSettings, + new SiteDomainHelper()); + + // get a snapshot, get a published content + var snapshot = snapshotService.CreatePublishedSnapshot(previewToken: null); + var publishedContent = snapshot.Content.GetById(1); + + // invariant is the current default + variationAccessor.VariationContext = new VariationContext(); + + Assert.IsNotNull(publishedContent); + Assert.AreEqual("It Works1!", publishedContent.Name); + Assert.AreEqual("val1", publishedContent.Value("prop")); + Assert.AreEqual("val-fr1", publishedContent.Value("prop", "fr-FR")); + Assert.AreEqual("val-uk1", publishedContent.Value("prop", "en-UK")); + + Assert.AreEqual("name-fr1", publishedContent.GetCulture("fr-FR").Name); + Assert.AreEqual("name-uk1", publishedContent.GetCulture("en-UK").Name); + + var draftContent = snapshot.Content.GetById(true, 1); + Assert.AreEqual("It Works2!", draftContent.Name); + Assert.AreEqual("val2", draftContent.Value("prop")); + Assert.AreEqual("val-fr2", draftContent.Value("prop", "fr-FR")); + Assert.AreEqual("val-uk2", draftContent.Value("prop", "en-UK")); + + Assert.AreEqual("name-fr2", draftContent.GetCulture("fr-FR").Name); + Assert.AreEqual("name-uk2", draftContent.GetCulture("en-UK").Name); + + // now french is default + variationAccessor.VariationContext = new VariationContext("fr-FR"); + Assert.AreEqual("val-fr1", publishedContent.Value("prop")); + Assert.AreEqual("name-fr1", publishedContent.GetCulture().Name); + Assert.AreEqual("name-fr1", publishedContent.Name); + Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.GetCulture().Date); + + // now uk is default + variationAccessor.VariationContext = new VariationContext("en-UK"); + Assert.AreEqual("val-uk1", publishedContent.Value("prop")); + Assert.AreEqual("name-uk1", publishedContent.GetCulture().Name); + Assert.AreEqual("name-uk1", publishedContent.Name); + Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.GetCulture().Date); + + // invariant needs to be retrieved explicitely, when it's not default + Assert.AreEqual("val1", publishedContent.Value("prop", culture: "")); + + // but, + // if the content type / property type does not vary, then it's all invariant again + // modify the content type and property type, notify the snapshot service + contentType.Variations = ContentVariation.InvariantNeutral; + propertyType.Variations = ContentVariation.InvariantNeutral; + snapshotService.Notify(new[] { new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain) }); + + // get a new snapshot (nothing changed in the old one), get the published content again + var anotherSnapshot = snapshotService.CreatePublishedSnapshot(previewToken: null); + var againContent = anotherSnapshot.Content.GetById(1); + + Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.Variations); + Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.GetPropertyType("prop").Variations); + + // now, "no culture" means "invariant" + Assert.AreEqual("It Works1!", againContent.Name); + Assert.AreEqual("val1", againContent.Value("prop")); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 4668a86c78..8244600994 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -96,8 +96,9 @@ namespace Umbraco.Tests.PublishedContent public void To_DataTable_With_Filter() { var doc = GetContent(true, 1); - //change a doc type alias - ((TestPublishedContent) doc.Children.ElementAt(0)).DocumentTypeAlias = "DontMatch"; + //change a doc type alias + var c = (TestPublishedContent) doc.Children.ElementAt(0); + c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); var dt = doc.ChildrenAsTable(Current.Services, "Child"); @@ -133,14 +134,12 @@ namespace Umbraco.Tests.PublishedContent CreateDate = DateTime.Now, CreatorId = 1, CreatorName = "Shannon", - DocumentTypeAlias = contentTypeAlias, - DocumentTypeId = 2, Id = 3, SortOrder = 4, TemplateId = 5, UpdateDate = DateTime.Now, Path = "-1,3", - UrlName = "home-page", + UrlSegment = "home-page", Name = "Page" + Guid.NewGuid().ToString(), Version = Guid.NewGuid(), WriterId = 1, @@ -175,6 +174,8 @@ namespace Umbraco.Tests.PublishedContent ((Collection) d.Properties).Add( new RawValueProperty(factory.CreatePropertyType("property3", 1), d, "value" + (indexVals + 2))); } + + d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); return d; } @@ -182,7 +183,9 @@ namespace Umbraco.Tests.PublishedContent // l8tr... private class TestPublishedContent : IPublishedContent { - public string Url { get; set; } + public string Url { get; set; } + public string GetUrl(string culture = null) => throw new NotSupportedException(); + public PublishedItemType ItemType { get; set; } IPublishedContent IPublishedContent.Parent @@ -201,10 +204,9 @@ namespace Umbraco.Tests.PublishedContent public int TemplateId { get; set; } public int SortOrder { get; set; } public string Name { get; set; } - public IReadOnlyDictionary CultureNames => throw new NotSupportedException(); - public string UrlName { get; set; } - public string DocumentTypeAlias { get; set; } - public int DocumentTypeId { get; set; } + public PublishedCultureInfos GetCulture(string culture = null) => throw new NotSupportedException(); + public IReadOnlyDictionary Cultures => throw new NotSupportedException(); + public string UrlSegment { get; set; } public string WriterName { get; set; } public string CreatorName { get; set; } public int WriterId { get; set; } @@ -240,10 +242,7 @@ namespace Umbraco.Tests.PublishedContent return property; } - public PublishedContentType ContentType - { - get { throw new NotImplementedException(); } - } + public PublishedContentType ContentType { get; set; } } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs index ab4b1138c5..acff953503 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs @@ -1,6 +1,8 @@ -using NUnit.Framework; +using System.Collections.Generic; +using NUnit.Framework; using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.Testing; using Umbraco.Web; @@ -10,13 +12,14 @@ namespace Umbraco.Tests.PublishedContent [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] public class PublishedContentExtensionTests : PublishedContentTestBase { - private UmbracoContext ctx; - private string xmlContent = ""; - private bool createContentTypes = true; + private UmbracoContext _ctx; + private string _xmlContent = ""; + private bool _createContentTypes = true; + private Dictionary _contentTypes; protected override string GetXmlContent(int templateId) { - return xmlContent; + return _xmlContent; } [Test] @@ -24,7 +27,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("inherited", false)); } @@ -33,7 +36,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("base", false), Is.False); } @@ -42,17 +45,17 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("inherited", true)); } [Test] public void IsDocumentType_Recursive_BaseType_ReturnsTrue() { - ContentTypesCache.GetPublishedContentTypeByAlias = null; // fixme this is not pretty InitializeInheritedContentTypes(); + ContentTypesCache.GetPublishedContentTypeByAlias = null; - var publishedContent = ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("base", true)); } @@ -61,14 +64,14 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("invalidbase", true), Is.False); } private void InitializeInheritedContentTypes() { - ctx = GetUmbracoContext("/", 1, null, true); - if (createContentTypes) + _ctx = GetUmbracoContext("/", 1, null, true); + if (_createContentTypes) { var contentTypeService = Current.Services.ContentTypeService; var baseType = new ContentType(-1) { Alias = "base", Name = "Base" }; @@ -76,10 +79,18 @@ namespace Umbraco.Tests.PublishedContent var inheritedType = new ContentType(baseType, contentTypeAlias) { Alias = contentTypeAlias, Name = "Inherited" }; contentTypeService.Save(baseType); contentTypeService.Save(inheritedType); - createContentTypes = false; + _contentTypes = new Dictionary + { + { baseType.Alias, new PublishedContentType(baseType, null) }, + { inheritedType.Alias, new PublishedContentType(inheritedType, null) } + }; + ContentTypesCache.GetPublishedContentTypeByAlias = alias => _contentTypes[alias]; + _createContentTypes = false; } - xmlContent = @" + ContentTypesCache.GetPublishedContentTypeByAlias = alias => _contentTypes[alias]; + + _xmlContent = @" diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 61c37d6a51..d0ac8b0216 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -18,6 +18,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.PublishedContent { @@ -74,8 +75,8 @@ namespace Umbraco.Tests.PublishedContent new WebSecurity(httpContext, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), - globalSettings, - ServiceContext.EntityService); + globalSettings, + new TestVariationContextAccessor()); return umbracoContext; } @@ -220,7 +221,7 @@ namespace Umbraco.Tests.PublishedContent Id = 1, SortOrder = 0, Name = "Content 1", - UrlName = "content-1", + UrlSegment = "content-1", Path = "/1", Level = 1, Url = "/content-1", @@ -243,7 +244,7 @@ namespace Umbraco.Tests.PublishedContent Id = 2, SortOrder = 1, Name = "Content 2", - UrlName = "content-2", + UrlSegment = "content-2", Path = "/2", Level = 1, Url = "/content-2", @@ -266,7 +267,7 @@ namespace Umbraco.Tests.PublishedContent Id = 3, SortOrder = 2, Name = "Content 2Sub", - UrlName = "content-2sub", + UrlSegment = "content-2sub", Path = "/3", Level = 1, Url = "/content-2sub", diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 04ed54d81c..9a0034a0ef 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -16,6 +16,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; +using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.PublishedContent @@ -32,7 +33,8 @@ namespace Umbraco.Tests.PublishedContent base.Compose(); Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); - Container.RegisterSingleton(); + Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( @@ -324,8 +326,8 @@ namespace Umbraco.Tests.PublishedContent public void GetPropertyValueRecursiveTest() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", true); - var nullVal = doc.Value("DoNotFindThis", true); + var rVal = doc.Value("testRecursive", recurse: true); + var nullVal = doc.Value("DoNotFindThis", recurse: true); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } @@ -364,9 +366,18 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Children_GroupBy_DocumentTypeAlias() { + var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { }); + var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { }); + var contentTypes = new Dictionary + { + { home.Alias, home }, + { custom.Alias, custom } + }; + ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; + var doc = GetNode(1046); - var found1 = doc.Children.GroupBy(x => x.DocumentTypeAlias).ToArray(); + var found1 = doc.Children.GroupBy(x => x.ContentType.Alias).ToArray(); Assert.AreEqual(2, found1.Length); Assert.AreEqual(2, found1.Single(x => x.Key.ToString() == "Home").Count()); @@ -376,10 +387,19 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Children_Where_DocumentTypeAlias() { + var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { }); + var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { }); + var contentTypes = new Dictionary + { + { home.Alias, home }, + { custom.Alias, custom } + }; + ContentTypesCache.GetPublishedContentTypeByAlias = alias => contentTypes[alias]; + var doc = GetNode(1046); - var found1 = doc.Children.Where(x => x.DocumentTypeAlias == "CustomDocument"); - var found2 = doc.Children.Where(x => x.DocumentTypeAlias == "Home"); + var found1 = doc.Children.Where(x => x.ContentType.Alias == "CustomDocument"); + var found2 = doc.Children.Where(x => x.ContentType.Alias == "Home"); Assert.AreEqual(1, found1.Count()); Assert.AreEqual(2, found2.Count()); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index adfc9f535a..9fcc6125bc 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -133,7 +133,6 @@ namespace Umbraco.Tests.PublishedContent } } - [Test] public void Do_Not_Find_In_Recycle_Bin() { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs index cfc0df25b7..66f7871a4b 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs @@ -2,8 +2,10 @@ using System.Collections; using System.Collections.ObjectModel; using System.Globalization; +using System.Linq; using Moq; using NUnit.Framework; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.TestHelpers; @@ -72,8 +74,6 @@ namespace Umbraco.Tests.PublishedContent var pc = new Mock(); pc.Setup(content => content.Id).Returns(1); pc.Setup(content => content.Name).Returns("test"); - pc.Setup(content => content.DocumentTypeId).Returns(2); - pc.Setup(content => content.DocumentTypeAlias).Returns("testAlias"); pc.Setup(content => content.WriterName).Returns("admin"); pc.Setup(content => content.CreatorName).Returns("admin"); pc.Setup(content => content.CreateDate).Returns(DateTime.Now); @@ -81,6 +81,7 @@ namespace Umbraco.Tests.PublishedContent pc.Setup(content => content.Path).Returns("-1,1"); pc.Setup(content => content.Parent).Returns(() => null); pc.Setup(content => content.Properties).Returns(new Collection()); + pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral)); return pc; } } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4b7a131bd0..e502097819 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -165,8 +165,6 @@ namespace Umbraco.Tests.PublishedContent IsDraft = false; ContentType = contentType; - DocumentTypeAlias = contentType.Alias; - DocumentTypeId = contentType.Id; } #endregion @@ -178,10 +176,9 @@ namespace Umbraco.Tests.PublishedContent public int TemplateId { get; set; } public int SortOrder { get; set; } public string Name { get; set; } - public IReadOnlyDictionary CultureNames => throw new NotSupportedException(); - public string UrlName { get; set; } - public string DocumentTypeAlias { get; private set; } - public int DocumentTypeId { get; private set; } + public PublishedCultureInfos GetCulture(string culture = null) => throw new NotSupportedException(); + public IReadOnlyDictionary Cultures => throw new NotSupportedException(); + public string UrlSegment { get; set; } public string WriterName { get; set; } public string CreatorName { get; set; } public int WriterId { get; set; } @@ -192,6 +189,7 @@ namespace Umbraco.Tests.PublishedContent public Guid Version { get; set; } public int Level { get; set; } public string Url { get; set; } + public string GetUrl(string culture = null) => throw new NotSupportedException(); public PublishedItemType ItemType { get { return PublishedItemType.Content; } } public bool IsDraft { get; set; } diff --git a/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs index 949467a6fe..5787d3e613 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs @@ -148,7 +148,7 @@ namespace Umbraco.Tests.Routing [TestCase("http://domain1.com/fr/1001-2-1", 100121, "fr-FR")] [TestCase("http://domain1.com/1001-3", 10013, "en-US")] - [TestCase("http://domain2.com/1002", 1002, null)] + [TestCase("http://domain2.com/1002", 1002, "")] [TestCase("http://domain3.com/", 1003, "en-US")] [TestCase("http://domain3.com/en", 10031, "en-US")] @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Routing [TestCase("http://domain3.com/1003-3", 10033, "en-US")] [TestCase("https://domain1.com/", 1001, "en-US")] - [TestCase("https://domain3.com/", 1001, null)] // because domain3 is explicitely set on http + [TestCase("https://domain3.com/", 1001, "")] // because domain3 is explicitely set on http public void Lookup_NestedDomains(string url, int expectedId, string expectedCulture) { diff --git a/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs b/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs index 83b28ace25..9139b28528 100644 --- a/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs +++ b/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs @@ -277,7 +277,7 @@ namespace Umbraco.Tests.Routing publishedRouter.FindDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture.Name); - + var finder = new ContentFinderByUrl(Logger); var result = finder.TryFindContent(frequest); @@ -304,7 +304,7 @@ namespace Umbraco.Tests.Routing [TestCase("http://domain1.com/fr", "fr-FR", 10012)] // domain takes over local wildcard at 10012 [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] // domain takes over local wildcard at 10012 - [TestCase("/1003", null, 1003)] // default culture (no domain) + [TestCase("/1003", "", 1003)] // default culture (no domain) [TestCase("/1003/1003-1", "nl-NL", 10031)] // wildcard on 10031 applies [TestCase("/1003/1003-1/1003-1-1", "nl-NL", 100311)] // wildcard on 10031 applies #endregion @@ -338,8 +338,60 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); } + // domains such as "/en" are natively supported, and when instanciating + // DomainAndUri for them, the host will come from the current request + // + private void SetDomains3() + { + SetupDomainServiceMock(new[] + { + new UmbracoDomain("/en") + { + Id = 1, + LanguageId = LangEngId, + RootContentId = 10011, + LanguageIsoCode = "en-US" + }, + new UmbracoDomain("/fr") + { + Id = 1, + LanguageId = LangFrId, + RootContentId = 10012, + LanguageIsoCode = "fr-FR" + } + }); + } - + #region Cases + [TestCase("http://domain1.com/en", "en-US", 10011)] + [TestCase("http://domain1.com/en/1001-1-1", "en-US", 100111)] + [TestCase("http://domain1.com/fr", "fr-FR", 10012)] + [TestCase("http://domain1.com/fr/1001-2-1", "fr-FR", 100121)] + #endregion + public void DomainGeneric(string inputUrl, string expectedCulture, int expectedNode) + { + SetDomains3(); + + var globalSettings = Mock.Get(TestObjects.GetGlobalSettings()); //this will modify the IGlobalSettings instance stored in the container + globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(false); + SettingsForTests.ConfigureSettings(globalSettings.Object); + + var umbracoContext = GetUmbracoContext(inputUrl, globalSettings:globalSettings.Object); + var publishedRouter = CreatePublishedRouter(Container); + var frequest = publishedRouter.CreateRequest(umbracoContext); + + // lookup domain + publishedRouter.FindDomain(frequest); + Assert.IsNotNull(frequest.Domain); + + Assert.AreEqual(expectedCulture, frequest.Culture.Name); + + var finder = new ContentFinderByUrl(Logger); + var result = finder.TryFindContent(frequest); + + Assert.IsTrue(result); + Assert.AreEqual(frequest.PublishedContent.Id, expectedNode); + } [Test] public void WithDefaultCulture() diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index d0b3622127..c64c70e65a 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -17,7 +17,10 @@ using Umbraco.Web.Routing; using Umbraco.Web.WebApi; using Umbraco.Core.Strings; using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Runtime; using Current = Umbraco.Web.Composing.Current; @@ -140,6 +143,9 @@ namespace Umbraco.Tests.Routing frequest.PublishedContent = umbracoContext.ContentCache.GetById(1172); frequest.TemplateModel = template; + var type = new AutoPublishedContentType(22, "CustomDocument", new PublishedPropertyType[] { }); + ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; + var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContext, Mock.Of())); handler.GetHandlerForRoute(umbracoContext.HttpContext.Request.RequestContext, frequest); diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index 185812002d..d3cd25ae92 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -5,6 +5,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; @@ -166,9 +167,15 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), + ContentVariation.CultureNeutral); + var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) .Returns("9876/home/test-fr"); //prefix with the root id node with the domain assigned as per the umbraco standard + publishedContentCache.Setup(x => x.GetById(It.IsAny())) + .Returns(id => id == 1234 ? publishedContent : null); var domainCache = new Mock(); domainCache.Setup(x => x.GetAssigned(It.IsAny(), false)) @@ -209,9 +216,15 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), + ContentVariation.CultureNeutral); + var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) .Returns("9876/home/test-fr"); //prefix with the root id node with the domain assigned as per the umbraco standard + publishedContentCache.Setup(x => x.GetById(It.IsAny())) + .Returns(id => id == 1234 ? publishedContent : null); var domainCache = new Mock(); domainCache.Setup(x => x.GetAssigned(It.IsAny(), false)) @@ -261,9 +274,15 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), + ContentVariation.CultureNeutral); + var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) .Returns("9876/home/test-fr"); //prefix with the root id node with the domain assigned as per the umbraco standard + publishedContentCache.Setup(x => x.GetById(It.IsAny())) + .Returns(id => id == 1234 ? publishedContent : null); var domainCache = new Mock(); domainCache.Setup(x => x.GetAssigned(It.IsAny(), false)) @@ -306,7 +325,6 @@ namespace Umbraco.Tests.Routing globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(false); SettingsForTests.ConfigureSettings(globalSettings.Object); - var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); diff --git a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs index b064e9685c..627d95ea29 100644 --- a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs @@ -190,7 +190,7 @@ namespace Umbraco.Tests.Routing SetDomains1(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); + var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current: currentUri); Assert.AreEqual(expected, result); } @@ -226,7 +226,7 @@ namespace Umbraco.Tests.Routing SetDomains2(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); + var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); Assert.AreEqual(expected, result); } @@ -254,7 +254,7 @@ namespace Umbraco.Tests.Routing SetDomains3(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); + var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); Assert.AreEqual(expected, result); } @@ -288,7 +288,7 @@ namespace Umbraco.Tests.Routing SetDomains4(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); + var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); Assert.AreEqual(expected, result); } @@ -312,17 +312,17 @@ namespace Umbraco.Tests.Routing SetDomains4(); string ignore; - ignore = umbracoContext.UrlProvider.GetUrl(1001, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(10011, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(100111, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(10012, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(100121, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(10013, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(1002, new Uri("http://domain1.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(1001, new Uri("http://domain2.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(10011, new Uri("http://domain2.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(100111, new Uri("http://domain2.com"), false); - ignore = umbracoContext.UrlProvider.GetUrl(1002, new Uri("http://domain2.com"), false); + ignore = umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10012, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10013, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain2.com")); var cache = umbracoContext.ContentCache as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); @@ -341,15 +341,15 @@ namespace Umbraco.Tests.Routing CheckRoute(cachedRoutes, cachedIds, 1002, "/1002"); // use the cache - Assert.AreEqual("/", umbracoContext.UrlProvider.GetUrl(1001, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/en/", umbracoContext.UrlProvider.GetUrl(10011, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/fr/", umbracoContext.UrlProvider.GetUrl(10012, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/1001-3/", umbracoContext.UrlProvider.GetUrl(10013, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/1002/", umbracoContext.UrlProvider.GetUrl(1002, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/", umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/en/", umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/fr/", umbracoContext.UrlProvider.GetUrl(10012, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/1001-3/", umbracoContext.UrlProvider.GetUrl(10013, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/1002/", umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("http://domain1.com/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, new Uri("http://domain2.com"), false)); + Assert.AreEqual("http://domain1.com/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain2.com"))); } private static void CheckRoute(IDictionary routes, IDictionary ids, int id, string route) diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index cc9813cdbd..d59fe0bb51 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -21,10 +21,12 @@ using Umbraco.Core.Services.Implement; using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Cache; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -88,10 +90,12 @@ namespace Umbraco.Tests.Scoping contentTypeFactory, null, publishedSnapshotAccessor, + Mock.Of(), Logger, ScopeProvider, documentRepository, mediaRepository, memberRepository, - SystemDefaultCultureProvider, + DefaultCultureAccessor, + new DatabaseDataSource(), Container.GetInstance(), new SiteDomainHelper()); } @@ -110,7 +114,7 @@ namespace Umbraco.Tests.Scoping umbracoSettings ?? SettingsForTests.GetDefaultUmbracoSettings(), urlProviders ?? Enumerable.Empty(), globalSettings, - Mock.Of()); + new TestVariationContextAccessor()); if (setSingleton) Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs index 51d6a7fa7d..11ef8a9411 100644 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs @@ -9,6 +9,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Services; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.Security Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), new List(),globalSettings, - Mock.Of()); + new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); var mgr = new BackOfficeCookieManager( @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Security Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), new List(), globalSettings, - Mock.Of()); + new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); var mgr = new BackOfficeCookieManager(Mock.Of(accessor => accessor.UmbracoContext == umbCtx), runtime, TestObjects.GetGlobalSettings()); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 229a8c5ac2..aa70b453d1 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -978,7 +978,7 @@ namespace Umbraco.Tests.Services var content = new Content(string.Empty, -1, ServiceContext.ContentTypeService.Get("umbTextpage")); // Act & Assert - Assert.Throws(() => contentService.Save(content)); + Assert.Throws(() => contentService.Save(content)); } [Test] @@ -2512,8 +2512,8 @@ namespace Umbraco.Tests.Services content.SetValue("author", "Barack Obama"); content.SetValue("prop", "value-fr1", langFr.IsoCode); content.SetValue("prop", "value-uk1", langUk.IsoCode); - content.SetName(langFr.IsoCode, "name-fr"); - content.SetName(langUk.IsoCode, "name-uk"); + content.SetName("name-fr", langFr.IsoCode); + content.SetName("name-uk", langUk.IsoCode); contentService.Save(content); // content has been saved, @@ -2562,7 +2562,11 @@ namespace Umbraco.Tests.Services Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); - Assert.IsNull(content2.PublishName); // we haven't published InvariantNeutral + // we haven't published InvariantNeutral, but a document cannot be published without an invariant name, + // so when we tried and published for the first time above the french culture, the french name was used + // to populate the invariant name + Assert.AreEqual("name-fr", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2583,8 +2587,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // note that content and content2 culture published dates might be slightly different due to roundtrip to database @@ -2603,9 +2607,9 @@ namespace Umbraco.Tests.Services // act - content.SetName(null, "Home US2"); - content.SetName(langFr.IsoCode, "name-fr2"); - content.SetName(langUk.IsoCode, "name-uk2"); + content.SetName("Home US2", null); + content.SetName("name-fr2", langFr.IsoCode); + content.SetName("name-uk2", langUk.IsoCode); content.SetValue("author", "Barack Obama2"); content.SetValue("prop", "value-fr2", langFr.IsoCode); content.SetValue("prop", "value-uk2", langUk.IsoCode); @@ -2644,8 +2648,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // act @@ -2687,8 +2691,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2734,8 +2738,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2774,8 +2778,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2797,13 +2801,13 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetDateCulturePublished(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act - content.SetName(langUk.IsoCode, "name-uk3"); + content.SetName("name-uk3", langUk.IsoCode); contentService.Save(content); content2 = contentService.GetById(content.Id); diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index cbcf0c3566..5cb59fecbe 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -19,22 +19,22 @@ namespace Umbraco.Tests.Services [Apartment(ApartmentState.STA)] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] public class EntityServiceTests : TestWithSomeContentBase - { - private Language _langFr; - private Language _langEs; - - public override void SetUp() - { - base.SetUp(); - - if (_langFr == null && _langEs == null) - { - _langFr = new Language("fr-FR"); - _langEs = new Language("es-ES"); - ServiceContext.LocalizationService.Save(_langFr); - ServiceContext.LocalizationService.Save(_langEs); - } - } + { + private Language _langFr; + private Language _langEs; + + public override void SetUp() + { + base.SetUp(); + + if (_langFr == null && _langEs == null) + { + _langFr = new Language("fr-FR"); + _langEs = new Language("es-ES"); + ServiceContext.LocalizationService.Save(_langFr); + ServiceContext.LocalizationService.Save(_langEs); + } + } [Test] public void EntityService_Can_Get_Paged_Content_Children() @@ -443,77 +443,77 @@ namespace Umbraco.Tests.Services Assert.That(entities.Any(), Is.True); Assert.That(entities.Length, Is.EqualTo(1)); Assert.That(entities.Any(x => x.Trashed), Is.False); - } - + } + [Test] public void EntityService_Can_Get_Content_By_UmbracoObjectType_With_Variant_Names() - { - var service = ServiceContext.EntityService; - - - var alias = "test" + Guid.NewGuid(); - var contentType = MockedContentTypes.CreateSimpleContentType(alias, alias, false); - contentType.Variations = ContentVariation.CultureNeutral; - ServiceContext.ContentTypeService.Save(contentType); - - var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); - c1.SetName(_langFr.IsoCode, "Test - FR"); - c1.SetName(_langEs.IsoCode, "Test - ES"); - ServiceContext.ContentService.Save(c1); - - var result = service.Get(c1.Id, UmbracoObjectTypes.Document); - Assert.AreEqual("Test", result.Name); - Assert.IsNotNull(result as IDocumentEntitySlim); - var doc = (IDocumentEntitySlim)result; - var cultureNames = doc.CultureNames; - Assert.AreEqual("Test - FR", cultureNames[_langFr.IsoCode]); - Assert.AreEqual("Test - ES", cultureNames[_langEs.IsoCode]); - } - + { + var service = ServiceContext.EntityService; + + + var alias = "test" + Guid.NewGuid(); + var contentType = MockedContentTypes.CreateSimpleContentType(alias, alias, false); + contentType.Variations = ContentVariation.CultureNeutral; + ServiceContext.ContentTypeService.Save(contentType); + + var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); + c1.SetName("Test - FR", _langFr.IsoCode); + c1.SetName("Test - ES", _langEs.IsoCode); + ServiceContext.ContentService.Save(c1); + + var result = service.Get(c1.Id, UmbracoObjectTypes.Document); + Assert.AreEqual("Test", result.Name); + Assert.IsNotNull(result as IDocumentEntitySlim); + var doc = (IDocumentEntitySlim)result; + var cultureNames = doc.CultureNames; + Assert.AreEqual("Test - FR", cultureNames[_langFr.IsoCode]); + Assert.AreEqual("Test - ES", cultureNames[_langEs.IsoCode]); + } + [Test] public void EntityService_Can_Get_Child_Content_By_ParentId_And_UmbracoObjectType_With_Variant_Names() { - var service = ServiceContext.EntityService; - - var contentType = MockedContentTypes.CreateSimpleContentType("test1", "Test1", false); - contentType.Variations = ContentVariation.CultureNeutral; - ServiceContext.ContentTypeService.Save(contentType); - + var service = ServiceContext.EntityService; + + var contentType = MockedContentTypes.CreateSimpleContentType("test1", "Test1", false); + contentType.Variations = ContentVariation.CultureNeutral; + ServiceContext.ContentTypeService.Save(contentType); + var root = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(root); - + ServiceContext.ContentService.Save(root); + for (int i = 0; i < 10; i++) { - var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); + var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); if (i % 2 == 0) { - c1.SetName(_langFr.IsoCode, "Test " + i + " - FR"); - c1.SetName(_langEs.IsoCode, "Test " + i + " - ES"); + c1.SetName("Test " + i + " - FR", _langFr.IsoCode); + c1.SetName("Test " + i + " - ES", _langEs.IsoCode); } ServiceContext.ContentService.Save(c1); } - var entities = service.GetChildren(root.Id, UmbracoObjectTypes.Document).ToArray(); - - Assert.AreEqual(10, entities.Length); - - for (int i = 0; i < entities.Length; i++) - { + var entities = service.GetChildren(root.Id, UmbracoObjectTypes.Document).ToArray(); + + Assert.AreEqual(10, entities.Length); + + for (int i = 0; i < entities.Length; i++) + { if (i % 2 == 0) { - Assert.AreEqual(1, entities[i].AdditionalData.Count); - var doc = (IDocumentEntitySlim)entities[i]; - var keys = doc.CultureNames.Keys.ToList(); - var vals = doc.CultureNames.Values.ToList(); - Assert.AreEqual(_langFr.Id, keys[0]); - Assert.AreEqual("Test " + i + " - FR", vals[0]); - Assert.AreEqual(_langEs.Id, keys[1]); + Assert.AreEqual(1, entities[i].AdditionalData.Count); + var doc = (IDocumentEntitySlim)entities[i]; + var keys = doc.CultureNames.Keys.ToList(); + var vals = doc.CultureNames.Values.ToList(); + Assert.AreEqual(_langFr.IsoCode.ToLowerInvariant(), keys[0].ToLowerInvariant()); + Assert.AreEqual("Test " + i + " - FR", vals[0]); + Assert.AreEqual(_langEs.IsoCode.ToLowerInvariant(), keys[1].ToLowerInvariant()); Assert.AreEqual("Test " + i + " - ES", vals[1]); - } - else - { - Assert.AreEqual(0, entities[i].AdditionalData.Count); - } + } + else + { + Assert.AreEqual(0, entities[i].AdditionalData.Count); + } } } diff --git a/src/Umbraco.Tests/Services/Importing/XsltSearch-Package.xml b/src/Umbraco.Tests/Services/Importing/XsltSearch-Package.xml index 8096cfb833..4839f3c0db 100644 --- a/src/Umbraco.Tests/Services/Importing/XsltSearch-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/XsltSearch-Package.xml @@ -161,10 +161,8 @@ p.xsltsearch_result_description {padding-bottom: 10px;} XSLTsearch XSLTsearch - - - - + Unknown + XSLTsearch.xslt False 0 @@ -251,4 +249,4 @@ p.xsltsearch_result_description {padding-bottom: 10px;} - \ No newline at end of file + diff --git a/src/Umbraco.Tests/Services/Importing/uBlogsy-Package.xml b/src/Umbraco.Tests/Services/Importing/uBlogsy-Package.xml index 98e12465f5..e1af13bb5e 100644 --- a/src/Umbraco.Tests/Services/Importing/uBlogsy-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/uBlogsy-Package.xml @@ -2033,10 +2033,8 @@ e.g Mid-code Crisis [uBlogsy] Show Some Love uBlogsyShowSomeLove - - - - + Unknown + True @@ -2160,4 +2158,4 @@ e.g Mid-code Crisis - \ No newline at end of file + diff --git a/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs b/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs index 543b4133f9..36e5874e14 100644 --- a/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs +++ b/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs @@ -43,7 +43,7 @@ namespace Umbraco.Tests.Strings StringType = CleanStringType.LowerCase | CleanStringType.Ascii, Separator = '-' }) - .WithConfig(new CultureInfo("fr-FR"), CleanStringType.UrlSegment, new DefaultShortStringHelperConfig.Config + .WithConfig("fr-FR", CleanStringType.UrlSegment, new DefaultShortStringHelperConfig.Config { PreFilter = FilterFrenchElisions, IsTerm = (c, leading) => leading ? char.IsLetter(c) : (char.IsLetterOrDigit(c) || c == '_'), @@ -56,7 +56,7 @@ namespace Umbraco.Tests.Strings IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c), StringType = CleanStringType.UmbracoCase | CleanStringType.Ascii }) - .WithConfig(new CultureInfo("fr-FR"), CleanStringType.Alias, new DefaultShortStringHelperConfig.Config + .WithConfig("fr-FR", CleanStringType.Alias, new DefaultShortStringHelperConfig.Config { PreFilter = WhiteQuotes, IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c), @@ -588,16 +588,12 @@ namespace Umbraco.Tests.Strings #endregion public void CleanStringWithTypeAndCulture(string input, string expected, string culture, CleanStringType stringType) { - var cinfo = culture == null ? CultureInfo.InvariantCulture : new CultureInfo(culture); - // picks the proper config per culture // and overrides some stringType params (ascii...) - var output = _helper.CleanString(input, stringType, cinfo); + var output = _helper.CleanString(input, stringType, culture); Assert.AreEqual(expected, output); } - - #region Cases [TestCase("foo.txt", "foo.txt")] [TestCase("foo", "foo")] diff --git a/src/Umbraco.Tests/Strings/MockShortStringHelper.cs b/src/Umbraco.Tests/Strings/MockShortStringHelper.cs index 964e1d7ad2..e0df7ea4e9 100644 --- a/src/Umbraco.Tests/Strings/MockShortStringHelper.cs +++ b/src/Umbraco.Tests/Strings/MockShortStringHelper.cs @@ -19,17 +19,7 @@ namespace Umbraco.Tests.Strings return "SAFE-ALIAS::" + text; } - public string CleanStringForSafeCamelAlias(string text) - { - return "SAFE-ALIAS::" + text; - } - - public string CleanStringForSafeAlias(string text, System.Globalization.CultureInfo culture) - { - return "SAFE-ALIAS-CULTURE::" + text; - } - - public string CleanStringForSafeCamelAlias(string text, System.Globalization.CultureInfo culture) + public string CleanStringForSafeAlias(string text, string culture) { return "SAFE-ALIAS-CULTURE::" + text; } @@ -39,7 +29,7 @@ namespace Umbraco.Tests.Strings return "URL-SEGMENT::" + text; } - public string CleanStringForUrlSegment(string text, System.Globalization.CultureInfo culture) + public string CleanStringForUrlSegment(string text, string culture) { return "URL-SEGMENT-CULTURE::" + text; } @@ -49,7 +39,7 @@ namespace Umbraco.Tests.Strings return "SAFE-FILE-NAME::" + text; } - public string CleanStringForSafeFileName(string text, System.Globalization.CultureInfo culture) + public string CleanStringForSafeFileName(string text, string culture) { return "SAFE-FILE-NAME-CULTURE::" + text; } @@ -69,12 +59,12 @@ namespace Umbraco.Tests.Strings return "CLEAN-STRING-B::" + text; } - public string CleanString(string text, CleanStringType stringType, System.Globalization.CultureInfo culture) + public string CleanString(string text, CleanStringType stringType, string culture) { return "CLEAN-STRING-C::" + text; } - public string CleanString(string text, CleanStringType stringType, char separator, System.Globalization.CultureInfo culture) + public string CleanString(string text, CleanStringType stringType, char separator, string culture) { return "CLEAN-STRING-D::" + text; } diff --git a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs index b297f27656..55976e0d89 100644 --- a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs +++ b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs @@ -188,7 +188,7 @@ namespace Umbraco.Tests.Strings [Test] public void ToSafeAliasWithCulture() { - var output = "JUST-ANYTHING".ToSafeAlias(CultureInfo.InvariantCulture); + var output = "JUST-ANYTHING".ToSafeAlias(null); Assert.AreEqual("SAFE-ALIAS-CULTURE::JUST-ANYTHING", output); } @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Strings [Test] public void ToUrlSegmentWithCulture() { - var output = "JUST-ANYTHING".ToUrlSegment(CultureInfo.InvariantCulture); + var output = "JUST-ANYTHING".ToUrlSegment(null); Assert.AreEqual("URL-SEGMENT-CULTURE::JUST-ANYTHING", output); } @@ -216,7 +216,7 @@ namespace Umbraco.Tests.Strings [Test] public void ToSafeFileNameWithCulture() { - var output = "JUST-ANYTHING".ToSafeFileName(CultureInfo.InvariantCulture); + var output = "JUST-ANYTHING".ToSafeFileName(null); Assert.AreEqual("SAFE-FILE-NAME-CULTURE::JUST-ANYTHING", output); } diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 97d88a680b..ded14c40ab 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -29,6 +29,7 @@ using Umbraco.Web.Security; using Umbraco.Web.WebApi; using LightInject; using System.Globalization; +using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.TestHelpers.ControllerTesting { @@ -149,11 +150,11 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == UrlProviderMode.Auto.ToString())), Enumerable.Empty(), globalSettings, - mockedEntityService, + new TestVariationContextAccessor(), true); //replace it var urlHelper = new Mock(); - urlHelper.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + urlHelper.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns("/hello/world/1234"); var membershipHelper = new MembershipHelper(umbCtx, Mock.Of(), Mock.Of()); diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs index 7d9bbec855..46ff57c1f3 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; @@ -10,19 +7,32 @@ namespace Umbraco.Tests.TestHelpers.Stubs { internal class TestPublishedContent : PublishedElement, IPublishedContent { - public TestPublishedContent(PublishedContentType contentType, int id, Guid key, Dictionary values, bool previewing, Dictionary cultureNames = null) + public TestPublishedContent(PublishedContentType contentType, int id, Guid key, Dictionary values, bool previewing, Dictionary cultures = null) : base(contentType, key, values, previewing) { Id = id; - CultureNames = cultureNames; + Cultures = cultures; } public int Id { get; } public int TemplateId { get; set; } public int SortOrder { get; set; } public string Name { get; set; } - public IReadOnlyDictionary CultureNames { get; set; } - public string UrlName { get; set; } + public IVariationContextAccessor VariationContextAccessor { get; set; } + public PublishedCultureInfos GetCulture(string culture = null) + { + // handle context culture + if (culture == null) + culture = VariationContextAccessor?.VariationContext.Culture; + + // no invariant culture infos + if (culture == "" || Cultures == null) return null; + + // get + return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; + } + public IReadOnlyDictionary Cultures { get; set; } + public string UrlSegment { get; set; } public string DocumentTypeAlias => ContentType.Alias; public int DocumentTypeId { get; set; } public string WriterName { get; set; } @@ -35,6 +45,7 @@ namespace Umbraco.Tests.TestHelpers.Stubs public Guid Version { get; set; } public int Level { get; set; } public string Url { get; set; } + public string GetUrl(string culture = null) => throw new NotSupportedException(); public PublishedItemType ItemType => ContentType.ItemType; public bool IsDraft { get; set; } public IPublishedContent Parent { get; set; } diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index ee0dd38c45..5745e501ab 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -119,7 +120,7 @@ namespace Umbraco.Tests.TestHelpers var urlProviders = Enumerable.Empty(); if (accessor == null) accessor = new TestUmbracoContextAccessor(); - return UmbracoContext.EnsureContext(accessor, httpContext, publishedSnapshotService, webSecurity, umbracoSettings, urlProviders, globalSettings, Mock.Of(), true); + return UmbracoContext.EnsureContext(accessor, httpContext, publishedSnapshotService, webSecurity, umbracoSettings, urlProviders, globalSettings, new TestVariationContextAccessor(), true); } public IUmbracoSettingsSection GetUmbracoSettings() diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 0fcd3c9295..25a0dc72fa 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -32,7 +32,7 @@ using LightInject; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Tests.Testing.Objects.AccessorsAndProviders; +using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.TestHelpers { @@ -78,7 +78,7 @@ namespace Umbraco.Tests.TestHelpers Container.Register(); Container.Register(factory => PublishedSnapshotService); - Container.Register(factory => SystemDefaultCultureProvider); + Container.Register(factory => DefaultCultureAccessor); Container.GetInstance() .Clear() @@ -231,7 +231,7 @@ namespace Umbraco.Tests.TestHelpers } } - protected ISystemDefaultCultureProvider SystemDefaultCultureProvider { get; set; } + protected IDefaultCultureAccessor DefaultCultureAccessor { get; set; } protected IPublishedSnapshotService PublishedSnapshotService { get; set; } @@ -239,7 +239,7 @@ namespace Umbraco.Tests.TestHelpers { base.Initialize(); - SystemDefaultCultureProvider = new TestSystemDefaultCultureProvider(); + DefaultCultureAccessor = new TestDefaultCultureAccessor(); CreateAndInitializeDatabase(); @@ -264,13 +264,14 @@ namespace Umbraco.Tests.TestHelpers // testing=true so XmlStore will not use the file nor the database var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); + var variationContextAccessor = new TestVariationContextAccessor(); var service = new PublishedSnapshotService( ServiceContext, Container.GetInstance(), ScopeProvider, - cache, publishedSnapshotAccessor, + cache, publishedSnapshotAccessor, variationContextAccessor, Container.GetInstance(), Container.GetInstance(), Container.GetInstance(), - SystemDefaultCultureProvider, + DefaultCultureAccessor, Logger, Container.GetInstance(), new SiteDomainHelper(), ContentTypesCache, @@ -378,7 +379,7 @@ namespace Umbraco.Tests.TestHelpers umbracoSettings ?? Container.GetInstance(), urlProviders ?? Enumerable.Empty(), globalSettings ?? Container.GetInstance(), - ServiceContext.EntityService); + new TestVariationContextAccessor()); if (setSingleton) Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; diff --git a/src/Umbraco.Tests/TestHelpers/NoHttpContextAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/NoHttpContextAccessor.cs similarity index 78% rename from src/Umbraco.Tests/TestHelpers/NoHttpContextAccessor.cs rename to src/Umbraco.Tests/Testing/Objects/Accessors/NoHttpContextAccessor.cs index b77f8f828c..9b37389241 100644 --- a/src/Umbraco.Tests/TestHelpers/NoHttpContextAccessor.cs +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/NoHttpContextAccessor.cs @@ -1,7 +1,7 @@ using System.Web; using Umbraco.Web; -namespace Umbraco.Tests.TestHelpers +namespace Umbraco.Tests.Testing.Objects.Accessors { public class NoHttpContextAccessor : IHttpContextAccessor { diff --git a/src/Umbraco.Tests/Testing/Objects/Accessors/TestDefaultCultureAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/TestDefaultCultureAccessor.cs new file mode 100644 index 0000000000..41bd70626c --- /dev/null +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/TestDefaultCultureAccessor.cs @@ -0,0 +1,15 @@ +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.Testing.Objects.Accessors +{ + public class TestDefaultCultureAccessor : IDefaultCultureAccessor + { + private string _defaultCulture = string.Empty; + + public string DefaultCulture + { + get => _defaultCulture; + set => _defaultCulture = value ?? string.Empty; + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedSnapshotAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/TestPublishedSnapshotAccessor.cs similarity index 79% rename from src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedSnapshotAccessor.cs rename to src/Umbraco.Tests/Testing/Objects/Accessors/TestPublishedSnapshotAccessor.cs index 3768803de2..c46915e3a0 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/TestPublishedSnapshotAccessor.cs @@ -1,6 +1,6 @@ using Umbraco.Web.PublishedCache; -namespace Umbraco.Tests.TestHelpers.Stubs +namespace Umbraco.Tests.Testing.Objects.Accessors { public class TestPublishedSnapshotAccessor : IPublishedSnapshotAccessor { diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestUmbracoContextAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/TestUmbracoContextAccessor.cs similarity index 77% rename from src/Umbraco.Tests/TestHelpers/Stubs/TestUmbracoContextAccessor.cs rename to src/Umbraco.Tests/Testing/Objects/Accessors/TestUmbracoContextAccessor.cs index eaf8912fda..da93218907 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestUmbracoContextAccessor.cs +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/TestUmbracoContextAccessor.cs @@ -1,6 +1,6 @@ using Umbraco.Web; -namespace Umbraco.Tests.TestHelpers.Stubs +namespace Umbraco.Tests.Testing.Objects.Accessors { public class TestUmbracoContextAccessor : IUmbracoContextAccessor { diff --git a/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs new file mode 100644 index 0000000000..3c7377f2cc --- /dev/null +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs @@ -0,0 +1,17 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Tests.Testing.Objects.Accessors +{ + /// + /// Provides an implementation of for tests. + /// + public class TestVariationContextAccessor : IVariationContextAccessor + { + /// + public VariationContext VariationContext + { + get; + set; + } + } +} diff --git a/src/Umbraco.Tests/Testing/Objects/AccessorsAndProviders/TestSystemDefaultCultureProvider.cs b/src/Umbraco.Tests/Testing/Objects/AccessorsAndProviders/TestSystemDefaultCultureProvider.cs deleted file mode 100644 index f7e5484500..0000000000 --- a/src/Umbraco.Tests/Testing/Objects/AccessorsAndProviders/TestSystemDefaultCultureProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Web.PublishedCache; - -namespace Umbraco.Tests.Testing.Objects.AccessorsAndProviders -{ - public class TestSystemDefaultCultureProvider : ISystemDefaultCultureProvider - { - public string DefaultCulture { get; set; } - } -} diff --git a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs new file mode 100644 index 0000000000..26bfff0e1a --- /dev/null +++ b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Scoping; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; + +namespace Umbraco.Tests.Testing.Objects +{ + internal class TestDataSource : IDataSource + { + private readonly Dictionary _kits; + + public TestDataSource(params ContentNodeKit[] kits) + : this((IEnumerable) kits) + { } + + public TestDataSource(IEnumerable kits) + { + _kits = kits.ToDictionary(x => x.Node.Id, x => x); + } + + public ContentNodeKit GetContentSource(IScope scope, int id) + => _kits.TryGetValue(id, out var kit) ? kit : default; + + public IEnumerable GetAllContentSources(IScope scope) + => _kits.Values; + + public IEnumerable GetBranchContentSources(IScope scope, int id) + { + throw new NotImplementedException(); + } + + public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) + => _kits.Values.Where(x => ids.Contains(x.ContentTypeId)); + + public ContentNodeKit GetMediaSource(IScope scope, int id) + { + throw new NotImplementedException(); + } + + public IEnumerable GetAllMediaSources(IScope scope) + { + throw new NotImplementedException(); + } + + public IEnumerable GetBranchMediaSources(IScope scope, int id) + { + throw new NotImplementedException(); + } + + public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index 59bc24ed10..f21a953ae7 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -1,15 +1,18 @@ using System; using System.Globalization; +using System.Linq; using System.Web.Security; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Dictionary; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -74,13 +77,18 @@ namespace Umbraco.Tests.Testing.TestingTests var umbracoContext = TestObjects.GetUmbracoContextMock(); var urlProviderMock = new Mock(); - urlProviderMock.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + urlProviderMock.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns("/hello/world/1234"); var urlProvider = urlProviderMock.Object; - var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }); + var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); - Assert.AreEqual("/hello/world/1234", theUrlProvider.GetUrl(1234)); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), + ContentVariation.InvariantNeutral); + var publishedContent = Mock.Of(); + Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); + + Assert.AreEqual("/hello/world/1234", theUrlProvider.GetUrl(publishedContent)); } } } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 003ba9ff4d..2f00d065cd 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -21,16 +21,19 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; using Umbraco.Web.Services; using Umbraco.Examine; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Composing.CompositionRoots; using Umbraco.Web._Legacy.Actions; using Current = Umbraco.Core.Composing.Current; @@ -385,6 +388,14 @@ namespace Umbraco.Tests.Testing SettingsForTests.Reset(); // fixme - should it be optional? Mapper.Reset(); + + // clear static events + DocumentRepository.ClearScopeEvents(); + MediaRepository.ClearScopeEvents(); + MemberRepository.ClearScopeEvents(); + ContentTypeService.ClearScopeEvents(); + MediaTypeService.ClearScopeEvents(); + MemberTypeService.ClearScopeEvents(); } #endregion diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 827bfef4b7..bb7f62c14e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -129,6 +129,8 @@ + + @@ -176,11 +178,12 @@ - + + - + @@ -195,10 +198,10 @@ - + - + diff --git a/src/Umbraco.Tests/UmbracoExamine/ExamineDemoDataContentService.cs b/src/Umbraco.Tests/UmbracoExamine/ExamineDemoDataContentService.cs index ffbd4f82d0..5bc1fbfdaa 100644 --- a/src/Umbraco.Tests/UmbracoExamine/ExamineDemoDataContentService.cs +++ b/src/Umbraco.Tests/UmbracoExamine/ExamineDemoDataContentService.cs @@ -58,7 +58,5 @@ namespace Umbraco.Tests.UmbracoExamine } private readonly XDocument _xContent; - - } } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 30bcc3766d..c7599e63e4 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -58,7 +58,8 @@ namespace Umbraco.Tests.UmbracoExamine m.SortOrder == (int)x.Attribute("sortOrder") && m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && - m.Name == (string)x.Attribute("nodeName") && + m.Name == (string)x.Attribute("nodeName") && + m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => @@ -102,6 +103,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime) x.Attribute("createDate") && m.UpdateDate == (DateTime) x.Attribute("updateDate") && m.Name == (string) x.Attribute("nodeName") && + m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string) x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 9d17be3476..8eb347c214 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -23,7 +23,6 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Rebuild_Index() { - using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) using (indexer.ProcessNonAsync()) @@ -46,7 +45,6 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { - using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext)) using (indexer.ProcessNonAsync()) @@ -178,7 +176,7 @@ namespace Umbraco.Tests.UmbracoExamine //create the whole thing indexer.RebuildIndex(); - + var result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); Assert.AreEqual(21, result.TotalItemCount); @@ -188,7 +186,7 @@ namespace Umbraco.Tests.UmbracoExamine { indexer.DeleteFromIndex(r.Id); } - + //ensure it's all gone result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); @@ -197,14 +195,11 @@ namespace Umbraco.Tests.UmbracoExamine //call our indexing methods indexer.IndexAll(IndexTypes.Content); - + result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); Assert.AreEqual(21, result.TotalItemCount); - } - - } /// @@ -221,15 +216,13 @@ namespace Umbraco.Tests.UmbracoExamine //create the whole thing indexer.RebuildIndex(); - + //now delete a node that has children indexer.DeleteFromIndex(1140.ToString()); //this node had children: 1141 & 1142, let's ensure they are also removed - - var results = searcher.Search(searcher.CreateCriteria().Id(1141).Compile()); Assert.AreEqual(0, results.Count()); @@ -240,6 +233,5 @@ namespace Umbraco.Tests.UmbracoExamine } private readonly ExamineDemoDataMediaService _mediaService = new ExamineDemoDataMediaService(); - } } diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index c71dc6f242..80dbe08343 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -23,7 +23,6 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Test_Sort_Order_Sorting() { - long totalRecs; var demoData = new ExamineDemoDataContentService(TestFiles.umbraco_sort); var allRecs = demoData.GetLatestContentByXPath("//*[@isDoc]") @@ -39,6 +38,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && m.Name == (string)x.Attribute("nodeName") && + m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.Published == true && diff --git a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs index 1f5fe1a6e3..f4c15f7c19 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Profiling; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Composing; using Umbraco.Web.Models; @@ -72,7 +73,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new MatchesDefaultIndexController { UmbracoContext = umbCtx }; var controllerCtx = new ControllerContext(req, ctrl); @@ -96,7 +97,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new MatchesOverriddenIndexController { UmbracoContext = umbCtx }; var controllerCtx = new ControllerContext(req, ctrl); @@ -120,7 +121,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new MatchesCustomIndexController { UmbracoContext = umbCtx }; var controllerCtx = new ControllerContext(req, ctrl); @@ -144,7 +145,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new MatchesAsyncIndexController { UmbracoContext = umbCtx }; var controllerCtx = new ControllerContext(req, ctrl); diff --git a/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs index 3b60f1a7c5..901c737584 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Current = Umbraco.Web.Composing.Current; diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 931cc57493..4186c56636 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Composing; using Umbraco.Web.Mvc; @@ -45,7 +46,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new TestSurfaceController { UmbracoContext = umbracoContext }; @@ -67,7 +68,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var ctrl = new TestSurfaceController { UmbracoContext = umbCtx }; @@ -87,7 +88,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var controller = new TestSurfaceController { UmbracoContext = umbracoContext }; @@ -114,7 +115,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "AutoLegacy")), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var helper = new UmbracoHelper( @@ -152,7 +153,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == webRoutingSettings), Enumerable.Empty(), globalSettings, - Mock.Of(), + new TestVariationContextAccessor(), true); var content = Mock.Of(publishedContent => publishedContent.Id == 12345); diff --git a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs index 295f42fee2..d5ac55827d 100644 --- a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs @@ -19,7 +19,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.AccessorsAndProviders; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Models; using Umbraco.Web.Mvc; @@ -424,23 +424,24 @@ namespace Umbraco.Tests.Web.Mvc //var provider = new ScopeUnitOfWorkProvider(databaseFactory, new RepositoryFactory(Mock.Of())); var scopeProvider = TestObjects.GetScopeProvider(Mock.Of()); var factory = Mock.Of(); - _service = new PublishedSnapshotService(svcCtx, factory, scopeProvider, cache, Enumerable.Empty(), null, - null, null, null, - new TestSystemDefaultCultureProvider(), + _service = new PublishedSnapshotService(svcCtx, factory, scopeProvider, cache, Enumerable.Empty(), + null, null, + null, null, null, + new TestDefaultCultureAccessor(), Current.Logger, TestObjects.GetGlobalSettings(), new SiteDomainHelper(), null, true, false); // no events var http = GetHttpContextFactory(url, routeData).HttpContext; - - var globalSettings = TestObjects.GetGlobalSettings(); + + var globalSettings = TestObjects.GetGlobalSettings(); var ctx = new UmbracoContext( http, _service, new WebSecurity(http, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), - Enumerable.Empty(), - globalSettings, - Mock.Of()); + Enumerable.Empty(), + globalSettings, + new TestVariationContextAccessor()); //if (setSingleton) //{ diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index d35b4e5823..c6f7d8551e 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Web; using LightInject; using Moq; @@ -10,9 +11,11 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -43,7 +46,7 @@ namespace Umbraco.Tests.Web Current.Container = container.Object; Umbraco.Web.Composing.Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); - + Udi.ResetUdiTypes(); } @@ -65,33 +68,43 @@ namespace Umbraco.Tests.Web //setup a mock entity service from the service context to return an integer for a GUID var entityService = Mock.Get(serviceCtxMock.EntityService); - entityService.Setup(x => x.GetId(It.IsAny(), It.IsAny())) - .Returns((Guid id, UmbracoObjectTypes objType) => - { - return Attempt.Succeed(1234); - }); + //entityService.Setup(x => x.GetId(It.IsAny(), It.IsAny())) + // .Returns((Guid id, UmbracoObjectTypes objType) => + // { + // return Attempt.Succeed(1234); + // }); //setup a mock url provider which we'll use fo rtesting var testUrlProvider = new Mock(); - testUrlProvider.Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((UmbracoContext umbCtx, int id, Uri url, UrlProviderMode mode, string culture) => - { - return "/my-test-url"; - }); + testUrlProvider + .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => "/my-test-url"); var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + var publishedContent = Mock.Of(); + Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); + Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); + var contentCache = Mock.Of(); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny())).Returns(publishedContent); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny())).Returns(publishedContent); + var snapshot = Mock.Of(); + Mock.Get(snapshot).Setup(x => x.Content).Returns(contentCache); + var snapshotService = Mock.Of(); + Mock.Get(snapshotService).Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot); + using (var umbCtx = UmbracoContext.EnsureContext( Umbraco.Web.Composing.Current.UmbracoContextAccessor, Mock.Of(), - Mock.Of(), + snapshotService, new Mock(null, null, globalSettings).Object, //setup a quick mock of the WebRouting section Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "AutoLegacy")), //pass in the custom url provider new[]{ testUrlProvider.Object }, globalSettings, - entityService.Object, + new TestVariationContextAccessor(), true)) { var output = TemplateUtilities.ParseInternalLinks(input, umbCtx.UrlProvider); diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs index cc97633cde..21b72a3832 100644 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; @@ -31,7 +32,7 @@ namespace Umbraco.Tests.Web TestObjects.GetUmbracoSettings(), new List(), TestObjects.GetGlobalSettings(), - Mock.Of()); + new TestVariationContextAccessor()); var r1 = new RouteData(); r1.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); @@ -49,7 +50,7 @@ namespace Umbraco.Tests.Web TestObjects.GetUmbracoSettings(), new List(), TestObjects.GetGlobalSettings(), - Mock.Of()); + new TestVariationContextAccessor()); var r1 = new RouteData(); r1.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); @@ -77,7 +78,7 @@ namespace Umbraco.Tests.Web TestObjects.GetUmbracoSettings(), new List(), TestObjects.GetGlobalSettings(), - Mock.Of()); + new TestVariationContextAccessor()); var httpContext = Mock.Of(); diff --git a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs b/src/Umbraco.Web/Cache/CacheRefresherComponent.cs index 07dcebd763..2ee0ff5f84 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherComponent.cs @@ -6,7 +6,6 @@ using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Services; using System.Linq; using System.Reflection; @@ -15,12 +14,13 @@ using System.Web.Hosting; using Umbraco.Core.Components; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; using Umbraco.Web.Composing; using Umbraco.Web.Security; using Umbraco.Web.Services; -using Content = Umbraco.Core.Models.Content; +using LightInject; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; namespace Umbraco.Web.Cache @@ -237,7 +237,7 @@ namespace Umbraco.Web.Cache UmbracoConfig.For.UmbracoSettings(), Current.UrlProviders, UmbracoConfig.For.GlobalSettings(), - Current.Services.EntityService, + Current.Container.GetInstance(), true); } diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index d327d1f70c..d295cf3d55 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -135,7 +135,7 @@ namespace Umbraco.Web.Composing internal static IPublishedSnapshotService PublishedSnapshotService => Container.GetInstance(); - + #endregion #region Web Constants @@ -247,6 +247,8 @@ namespace Umbraco.Web.Composing public static IPublishedContentTypeFactory PublishedContentTypeFactory => CoreCurrent.PublishedContentTypeFactory; + public static IPublishedValueFallback PublishedValueFallback => CoreCurrent.PublishedValueFallback; + #endregion } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 5f62b69e96..d038f9b975 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -72,8 +72,8 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingContent(typeof(IEnumerable))] public IEnumerable GetByIds([FromUri]int[] ids) - { - //fixme what about cultures? + { + //fixme what about cultures? var foundContent = Services.ContentService.GetByIds(ids); return foundContent.Select(x => MapToDisplay(x)); @@ -219,7 +219,7 @@ namespace Umbraco.Web.Editors return display; } - + //fixme what about cultures? public ContentItemDisplay GetBlueprintById(int id) { @@ -268,7 +268,7 @@ namespace Umbraco.Web.Editors HandleContentNotFound(id); return null;//irrelevant since the above throws } - + var content = MapToDisplay(foundContent, culture); return content; } @@ -1003,12 +1003,12 @@ namespace Umbraco.Web.Editors { //Don't update the name if it is empty if (contentItem.Name.IsNullOrWhiteSpace() == false) - { + { //set the name according to the culture settings if (contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - { + { if (contentItem.Culture.IsNullOrWhiteSpace()) throw new InvalidOperationException($"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified"); - contentItem.PersistedContent.SetName(contentItem.Culture, contentItem.Name); + contentItem.PersistedContent.SetName(contentItem.Name, contentItem.Culture); } else { @@ -1237,10 +1237,10 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content, string culture = null) { - //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, + //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, // the Cuture property of ContentItemDisplay must exist (at least currently). if (culture == null && content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) - { + { //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); } diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index af30f0fc28..b0af03fe61 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -12,6 +12,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Macros; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Editors { @@ -26,6 +27,13 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class MacroController : UmbracoAuthorizedJsonController, IRequiresSessionState { + private readonly IVariationContextAccessor _variationContextAccessor; + + public MacroController(IVariationContextAccessor variationContextAccessor) + { + _variationContextAccessor = variationContextAccessor; + } + /// /// Gets the macro parameters to be filled in for a particular macro /// @@ -113,7 +121,7 @@ namespace Umbraco.Web.Editors //the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that //and then set the legacy parameters. - var legacyPage = new global::umbraco.page(doc); + var legacyPage = new global::umbraco.page(doc, _variationContextAccessor); UmbracoContext.HttpContext.Items["pageID"] = doc.Id; UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements; UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null; diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index d51a00b3a2..26ca518259 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -90,7 +90,7 @@ namespace Umbraco.Web.Editors { timer.Start(); - pointerNode = pointerNode.FirstChild(x => x.DocumentTypeAlias == contentTypeAlias); + pointerNode = pointerNode.FirstChild(x => x.ContentType.Alias == contentTypeAlias); if (pointerNode == null) break; @@ -277,7 +277,7 @@ namespace Umbraco.Web.Editors if (targetNode == null || targetNode.Id == current.Id) return aliases; if (targetNode.Id != current.Id) { - aliases.Add(targetNode.DocumentTypeAlias); + aliases.Add(targetNode.ContentType.Alias); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs new file mode 100644 index 0000000000..47e4b3d872 --- /dev/null +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -0,0 +1,92 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Models.PublishedContent +{ + /// + /// Provides a default implementation for . + /// + public class PublishedValueFallback : IPublishedValueFallback + { + // this is our default implementation + // kinda reproducing what was available in v7 + + /// + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + { + // no fallback here + return defaultValue; + } + + /// + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + { + // no fallback here + return defaultValue; + } + + /// + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + { + // no fallback here + return defaultValue; + } + + /// + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + { + // no fallback here + return defaultValue; + } + + /// + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) + { + // no fallback here + if (!recurse) return defaultValue; + + // is that ok? + return GetValue(content, alias, culture, segment, defaultValue, recurse); + } + + /// + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) + { + // no fallback here + if (!recurse) return defaultValue; + + // otherwise, implement recursion as it was implemented in PublishedContentBase + + // fixme caching? + // + // all caches were using PublishedContentBase.GetProperty(alias, recurse) to get the property, + // then, + // NuCache.PublishedContent was storing the property in GetAppropriateCache() with key "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; + // XmlPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; + // DictionaryPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedMediaCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; + // + // at the moment, caching has been entirely removed, until we better understand caching + fallback + + IPublishedProperty property = null; // if we are here, content's property has no value + IPublishedProperty noValueProperty = null; + do + { + content = content.Parent; + property = content?.GetProperty(alias); + if (property != null) noValueProperty = property; + } while (content != null && (property == null || property.HasValue(culture, segment) == false)); + + // if we found a content with the property having a value, return that property value + if (property != null && property.HasValue(culture, segment)) + return property.Value(culture, segment); + + // if we found a property, even though with no value, return that property value + // because the converter may want to handle the missing value. ie if defaultValue is default, + // either specified or by default, the converter may want to substitute something else. + if (noValueProperty != null) + return noValueProperty.Value(culture, segment, defaultValue: defaultValue); + + // else return default + return defaultValue; + } + } +} diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index a89cab2c65..ffa72796a9 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; @@ -19,39 +15,87 @@ namespace Umbraco.Web.Models [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] public abstract class PublishedContentBase : IPublishedContent { - #region Content + private string _url; // fixme - cannot cache urls, they depends on the current request + + #region ContentType - private string _url; + public abstract PublishedContentType ContentType { get; } - /// - /// Gets the url of the content. - /// + #endregion + + #region PublishedElement + + /// + public abstract Guid Key { get; } + + #endregion + + #region PublishedContent + + /// + public abstract int Id { get; } + + /// + public abstract string Name { get; } + + /// + public abstract string UrlSegment { get; } + + /// + public abstract int SortOrder { get; } + + /// + public abstract int Level { get; } + + /// + public abstract string Path { get; } + + /// + public abstract int TemplateId { get; } + + /// + public abstract int CreatorId { get; } + + /// + public abstract string CreatorName { get; } + + /// + public abstract DateTime CreateDate { get; } + + /// + public abstract int WriterId { get; } + + /// + public abstract string WriterName { get; } + + /// + public abstract DateTime UpdateDate { get; } + + /// + public virtual string Url => GetUrl(); + + /// /// - /// If this content is Content, the url that is returned is the one computed by the NiceUrlProvider, otherwise if - /// this content is Media, the url returned is the value found in the 'umbracoFile' property. + /// The url of documents are computed by the document url providers. The url of medias are, at the moment, + /// computed here from the 'umbracoFile' property -- but we should move to media url providers at some point. /// - public virtual string Url + public virtual string GetUrl(string culture = null) // fixme - consider .GetCulture("fr-FR").Url { - get - { - // should be thread-safe although it won't prevent url from being resolved more than once - if (_url != null) - return _url; - switch (ItemType) { - case PublishedItemType.Content: + case PublishedItemType.Content: + // fixme - consider injecting an umbraco context accessor if (UmbracoContext.Current == null) - throw new InvalidOperationException( - "Cannot resolve a Url for a content item when UmbracoContext.Current is null."); + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.Current is null."); if (UmbracoContext.Current.UrlProvider == null) - throw new InvalidOperationException( - "Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); - _url = UmbracoContext.Current.UrlProvider.GetUrl(Id); - break; + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.Current.UrlProvider is null."); + return UmbracoContext.Current.UrlProvider.GetUrl(this); + case PublishedItemType.Media: + if (_url != null) return _url; // assume it will not depend on current uri/culture + var prop = GetProperty(Constants.Conventions.Media.File); - if (prop == null || prop.GetValue() == null) + if (prop?.GetValue() == null) { _url = string.Empty; return _url; @@ -59,7 +103,8 @@ namespace Umbraco.Web.Models var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - // fixme this is horrible we need url providers for media too + // fixme - consider implementing media url providers + // note: that one does not support variations //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always // want to return the normal URL and the cropper stores data as json switch (propType.EditorAlias) @@ -79,113 +124,46 @@ namespace Umbraco.Web.Models _url = prop.GetValue()?.ToString(); break; } - break; + + return _url; + default: throw new NotSupportedException(); } + } + + /// + public abstract PublishedCultureInfos GetCulture(string culture = null); - return _url; - } - } + /// + public abstract IReadOnlyDictionary Cultures { get; } + /// public abstract PublishedItemType ItemType { get; } - public abstract int Id { get; } - public abstract Guid Key { get; } - public abstract int TemplateId { get; } - public abstract int SortOrder { get; } - public abstract string Name { get; } - //TODO: On the base ContentData instance this dictionary contains a CultureVariation, should we expose that model here or a different model? - public abstract IReadOnlyDictionary CultureNames { get; } - public abstract string UrlName { get; } - public abstract string DocumentTypeAlias { get; } - public abstract int DocumentTypeId { get; } - public abstract string WriterName { get; } - public abstract string CreatorName { get; } - public abstract int WriterId { get; } - public abstract int CreatorId { get; } - public abstract string Path { get; } - public abstract DateTime CreateDate { get; } - public abstract DateTime UpdateDate { get; } - public abstract int Level { get; } + /// public abstract bool IsDraft { get; } #endregion #region Tree - /// - /// Gets the parent of the content. - /// + /// public abstract IPublishedContent Parent { get; } - /// - /// Gets the children of the content. - /// - /// Children are sorted by their sortOrder. + /// public abstract IEnumerable Children { get; } #endregion - #region ContentType - - public abstract PublishedContentType ContentType { get; } - - #endregion - #region Properties - /// - /// Gets the properties of the content. - /// + /// public abstract IEnumerable Properties { get; } - /// - /// Gets a property identified by its alias. - /// - /// The property alias. - /// The property identified by the alias. - /// - /// If no property with the specified alias exists, returns null. - /// The returned property may have no value (ie HasValue is false). - /// The alias is case-insensitive. - /// + /// public abstract IPublishedProperty GetProperty(string alias); - /// - /// Gets a property identified by its alias. - /// - /// The property alias. - /// A value indicating whether to navigate the tree upwards until a property with a value is found. - /// The property identified by the alias. - /// - /// Navigate the tree upwards and look for a property with that alias and with a value (ie HasValue is true). - /// If found, return the property. If no property with that alias is found, having a value or not, return null. Otherwise - /// return the first property that was found with the alias but had no value (ie HasValue is false). - /// The alias is case-insensitive. - /// - public virtual IPublishedProperty GetProperty(string alias, bool recurse) - { - var property = GetProperty(alias); - if (recurse == false) return property; - - IPublishedContent content = this; - var firstNonNullProperty = property; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content?.GetProperty(alias); - if (firstNonNullProperty == null && property != null) firstNonNullProperty = property; - } - - // if we find a content with the property with a value, return that property - // if we find no content with the property, return null - // if we find a content with the property without a value, return that property - // have to save that first property while we look further up, hence firstNonNullProperty - - return property != null && property.HasValue() ? property : firstNonNullProperty; - } - #endregion } } diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 3b0b6b35e2..446827f0a3 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -300,7 +300,7 @@ namespace Umbraco.Web.Mvc } //check if there's a custom controller assigned, base on the document type alias. - var controllerType = _controllerFactory.GetControllerTypeInternal(requestContext, request.PublishedContent.DocumentTypeAlias); + var controllerType = _controllerFactory.GetControllerTypeInternal(requestContext, request.PublishedContent.ContentType.Alias); //check if that controller exists if (controllerType != null) @@ -320,7 +320,7 @@ namespace Umbraco.Web.Mvc else { Current.Logger.Warn(() => - $"The current Document Type {request.PublishedContent.DocumentTypeAlias} matches a locally declared controller of type {controllerType.FullName}. Custom Controllers for Umbraco routing must implement '{typeof(IRenderController).FullName}' and inherit from '{typeof(ControllerBase).FullName}'."); + $"The current Document Type {request.PublishedContent.ContentType.Alias} matches a locally declared controller of type {controllerType.FullName}. Custom Controllers for Umbraco routing must implement '{typeof(IRenderController).FullName}' and inherit from '{typeof(ControllerBase).FullName}'."); //we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults // that have already been set above. diff --git a/src/Umbraco.Web/PublishedCache/SystemDefaultCultureProvider.cs b/src/Umbraco.Web/PublishedCache/DefaultCultureAccessor.cs similarity index 53% rename from src/Umbraco.Web/PublishedCache/SystemDefaultCultureProvider.cs rename to src/Umbraco.Web/PublishedCache/DefaultCultureAccessor.cs index 6838d483b0..80d2d9f3a5 100644 --- a/src/Umbraco.Web/PublishedCache/SystemDefaultCultureProvider.cs +++ b/src/Umbraco.Web/PublishedCache/DefaultCultureAccessor.cs @@ -3,22 +3,22 @@ namespace Umbraco.Web.PublishedCache { /// - /// Provides the default implementation of . + /// Provides the default implementation of . /// - public class SystemDefaultCultureProvider : ISystemDefaultCultureProvider + public class DefaultCultureAccessor : IDefaultCultureAccessor { private readonly ILocalizationService _localizationService; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - public SystemDefaultCultureProvider(ILocalizationService localizationService) + public DefaultCultureAccessor(ILocalizationService localizationService) { _localizationService = localizationService; } /// - public string DefaultCulture => _localizationService.GetDefaultLanguageIsoCode(); // capture - fast + public string DefaultCulture => _localizationService.GetDefaultLanguageIsoCode() ?? ""; // fast } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/IDefaultCultureAccessor.cs b/src/Umbraco.Web/PublishedCache/IDefaultCultureAccessor.cs new file mode 100644 index 0000000000..b1c1edd4ee --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/IDefaultCultureAccessor.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Web.PublishedCache +{ + /// + /// Gives access to the default culture. + /// + public interface IDefaultCultureAccessor + { + /// + /// Gets the system default culture. + /// + /// + /// Implementations must NOT return a null value. Return an empty string for the invariant culture. + /// + string DefaultCulture { get; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/IPublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/IPublishedSnapshot.cs index 628a67b561..0636eb808c 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedSnapshot.cs @@ -33,11 +33,18 @@ namespace Umbraco.Web.PublishedCache /// /// Gets the snapshot-level cache. /// + /// + /// The snapshot-level cache belongs to this snapshot only. + /// ICacheProvider SnapshotCache { get; } /// /// Gets the elements-level cache. /// + /// + /// The elements-level cache is shared by all snapshots relying on the same elements, + /// ie all snapshots built on top of unchanging content / media / etc. + /// ICacheProvider ElementsCache { get; } /// diff --git a/src/Umbraco.Web/PublishedCache/ISystemDefaultCultureProvider.cs b/src/Umbraco.Web/PublishedCache/ISystemDefaultCultureProvider.cs deleted file mode 100644 index 022b6d3e25..0000000000 --- a/src/Umbraco.Web/PublishedCache/ISystemDefaultCultureProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides the system default culture. - /// - public interface ISystemDefaultCultureProvider - { - /// - /// Gets the system default culture. - /// - string DefaultCulture { get; } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs b/src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs index 5bdee5a416..0b67c3a1e2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs @@ -43,11 +43,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return "NuCache.Profile.Name[" + userId + "]"; } - public static string PropertyRecurse(Guid contentUid, string typeAlias, bool previewing) - { - return "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; - } - public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) { return "NuCache.Property.CacheValues[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 0341d2dcf5..576677d6ac 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -104,8 +104,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // hideTopLevelNode = support legacy stuff, look for /*/path/to/node // else normal, look for /path/to/node content = hideTopLevelNode.Value - ? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.GetUrlName(_localizationService, culture) == parts[0]) - : GetAtRoot(preview).FirstOrDefault(x => x.GetUrlName(_localizationService, culture) == parts[0]); + ? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.GetCulture(culture).UrlSegment == parts[0]) + : GetAtRoot(preview).FirstOrDefault(x => x.GetCulture(culture).UrlSegment == parts[0]); content = FollowRoute(content, parts, 1, culture); } @@ -114,7 +114,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). if (content == null && hideTopLevelNode.Value && parts.Length == 1) { - content = GetAtRoot(preview).FirstOrDefault(x => x.GetUrlName(_localizationService, culture) == parts[0]); + content = GetAtRoot(preview).FirstOrDefault(x => x.GetCulture(culture).UrlSegment == parts[0]); } return content; @@ -147,14 +147,20 @@ namespace Umbraco.Web.PublishedCache.NuCache var hasDomains = _domainHelper.NodeHasDomains(n.Id); while (hasDomains == false && n != null) // n is null at root { - var urlName = n.GetUrlName(_localizationService, culture); - if (urlName == null) + var varies = n.ContentType.Variations.Has(ContentVariation.CultureNeutral); + var urlSegment = varies ? n.GetCulture(culture)?.UrlSegment : n.UrlSegment; + if (urlSegment.IsNullOrWhiteSpace()) { //we cannot continue, it will be null if the item is not published return null; } + + //// at that point we should have an urlSegment, unless something weird is happening + //// at content level, such as n.GetCulture() returning null for some (weird) reason, + //// and then what? fallback to the invariant segment... far from perfect but eh... + //if (string.IsNullOrWhiteSpace(urlSegment)) urlSegment = n.UrlSegment; - pathParts.Add(urlName); + pathParts.Add(urlSegment); // move to parent node n = n.Parent; @@ -183,8 +189,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var part = parts[i++]; content = content.Children.FirstOrDefault(x => { - var urlName = x.GetUrlName(_localizationService, culture); - return urlName == part; + var urlSegment = x.GetCulture(culture).UrlSegment; + return urlSegment == part; }); } return content; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index a7cc1e950c..2c2101b066 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -33,10 +33,11 @@ namespace Umbraco.Web.PublishedCache.NuCache int parentContentId, DateTime createDate, int creatorId, ContentData draftData, ContentData publishedData, - IPublishedSnapshotAccessor publishedSnapshotAccessor) + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IVariationContextAccessor variationContextAccessor) : this(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId) { - SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor); + SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor); } // 2-phases ctor, phase 1 @@ -58,7 +59,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } // two-phase ctor, phase 2 - public void SetContentTypeAndData(PublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public void SetContentTypeAndData(PublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { ContentType = contentType; @@ -66,9 +67,9 @@ namespace Umbraco.Web.PublishedCache.NuCache throw new ArgumentException("Both draftData and publishedData cannot be null at the same time."); if (draftData != null) - Draft = new PublishedContent(this, draftData, publishedSnapshotAccessor).CreateModel(); + Draft = new PublishedContent(this, draftData, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); if (publishedData != null) - Published = new PublishedContent(this, publishedData, publishedSnapshotAccessor).CreateModel(); + Published = new PublishedContent(this, publishedData, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); } // clone parent @@ -97,7 +98,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } // clone with new content type - public ContentNode(ContentNode origin, PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public ContentNode(ContentNode origin, PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { Id = origin.Id; Uid = origin.Uid; @@ -112,8 +113,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var originDraft = origin.Draft == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Draft); var originPublished = origin.Published == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Published); - Draft = originDraft == null ? null : new PublishedContent(this, originDraft._contentData, publishedSnapshotAccessor).CreateModel(); - Published = originPublished == null ? null : new PublishedContent(this, originPublished._contentData, publishedSnapshotAccessor).CreateModel(); + Draft = originDraft == null ? null : new PublishedContent(this, originDraft._contentData, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); + Published = originPublished == null ? null : new PublishedContent(this, originPublished._contentData, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); ChildContentIds = origin.ChildContentIds; // can be the *same* list FIXME oh really? } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index 82bfc8766a..f284d54cf1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -17,9 +17,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; - public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { - Node.SetContentTypeAndData(contentType, DraftData, PublishedData, publishedSnapshotAccessor); + Node.SetContentTypeAndData(contentType, DraftData, PublishedData, publishedSnapshotAccessor, variationContextAccessor); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 74613509ba..8a22259204 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // SnapDictionary has unit tests to ensure it all works correctly private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; private readonly ConcurrentDictionary> _contentNodes; private readonly ConcurrentDictionary> _contentRootNodes; private readonly ConcurrentDictionary> _contentTypesById; @@ -43,9 +44,10 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Ctor - public ContentStore(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger, BPlusTree localDb = null) + public ContentStore(IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, ILogger logger, BPlusTree localDb = null) { _publishedSnapshotAccessor = publishedSnapshotAccessor; + _variationContextAccessor = variationContextAccessor; _logger = logger; _localDb = localDb; @@ -277,7 +279,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (node == null) continue; var contentTypeId = node.ContentType.Id; if (index.TryGetValue(contentTypeId, out PublishedContentType contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType, _publishedSnapshotAccessor)); + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType, _publishedSnapshotAccessor, _variationContextAccessor)); } } finally @@ -391,7 +393,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentNodes.TryGetValue(id, out LinkedNode link); if (link?.Value == null) continue; - var node = new ContentNode(link.Value, contentType, _publishedSnapshotAccessor); + var node = new ContentNode(link.Value, contentType, _publishedSnapshotAccessor, _variationContextAccessor); SetValueLocked(_contentNodes, id, node); if (_localDb != null) RegisterChange(id, node.ToKit()); } @@ -414,7 +416,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; // and use - kit.Build(link.Value, _publishedSnapshotAccessor); + kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor); return true; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index a835886254..ccd0e18dd7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -9,16 +9,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IReadOnlyDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(); - // read variations count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + if (pcount == 0) return Empty; // read each variation + var dict = new Dictionary(); for (var i = 0; i < pcount; i++) { var languageId = PrimitiveSerializer.String.ReadFrom(stream); - var cultureVariation = new CultureVariation { Name = ReadStringObject(stream) }; + var cultureVariation = new CultureVariation { Name = ReadStringObject(stream), Date = ReadDateTime(stream) }; dict[languageId] = cultureVariation; } return dict; @@ -40,6 +40,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.String.WriteTo(culture, stream); // should never be null WriteObject(variation.Name, stream); // write an object in case it's null (though... should not happen) + PrimitiveSerializer.DateTime.WriteTo(variation.Date, stream); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs index 6841937c14..4721a1f4ca 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs @@ -6,20 +6,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // represents everything that is specific to edited or published version internal class ContentData { + public string Name { get; set; } + public int VersionId { get; set; } + public DateTime VersionDate { get; set; } + public int WriterId { get; set; } + public int TemplateId { get; set; } public bool Published { get; set; } + public IDictionary Properties { get; set; } + /// /// The collection of language Id to name for the content item /// public IReadOnlyDictionary CultureInfos { get; set; } - - public string Name { get; set; } - public int VersionId { get; set; } - //TODO: This will not make a lot of sense since we'll have dates for each variant publishing, need to wait on Stephane - public DateTime VersionDate { get; set; } - public int WriterId { get; set; } - public int TemplateId { get; set; } - - public IDictionary Properties { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs index aad416a925..50a2adaeb8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -10,6 +11,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource [JsonProperty("name")] public string Name { get; set; } - //TODO: We may want some date stamps here + [JsonProperty("date")] + public DateTime Date { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs similarity index 97% rename from src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs rename to src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 342ad5b59f..16f11aeafd 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // fixme - use SqlTemplate for these queries else it's going to be horribly slow! // provides efficient database access for NuCache - internal class Database + internal class DatabaseDataSource : IDataSource { // we want arrays, we want them all loaded, not an enumerable @@ -186,7 +186,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { if (Debugger.IsAttached) throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + Current.Logger.Warn("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); } else { @@ -211,7 +211,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { if (Debugger.IsAttached) throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); + Current.Logger.Warn("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); } else { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs new file mode 100644 index 0000000000..323d954980 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Umbraco.Core.Scoping; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// Defines a data source for NuCache. + /// + internal interface IDataSource + { + ContentNodeKit GetContentSource(IScope scope, int id); + IEnumerable GetAllContentSources(IScope scope); + IEnumerable GetBranchContentSources(IScope scope, int id); + IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids); + + ContentNodeKit GetMediaSource(IScope scope, int id); + IEnumerable GetAllMediaSources(IScope scope); + IEnumerable GetBranchMediaSources(IScope scope, int id); + IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs index ddb9607575..9f0b3cf7fa 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs @@ -1,14 +1,26 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal class PropertyData { + private string _culture; + private string _segment; + [JsonProperty("culture")] - public string Culture { get; set; } + public string Culture + { + get => _culture; + set => _culture = value ?? throw new ArgumentNullException(nameof(value)); + } [JsonProperty("seg")] - public string Segment { get; set; } + public string Segment + { + get => _segment; + set => _segment = value ?? throw new ArgumentNullException(nameof(value)); + } [JsonProperty("val")] public object Value { get; set; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs index a8cf4a97c0..ce8b5835e2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.PublishedCache.NuCache class MemberCache : IPublishedMemberCache, INavigableData { private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + public readonly IVariationContextAccessor VariationContextAccessor; private readonly ICacheProvider _snapshotCache; private readonly IMemberService _memberService; private readonly IDataTypeService _dataTypeService; @@ -23,10 +24,11 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly PublishedContentTypeCache _contentTypeCache; private readonly bool _previewDefault; - public MemberCache(bool previewDefault, ICacheProvider snapshotCache, IMemberService memberService, IDataTypeService dataTypeService, ILocalizationService localizationService, PublishedContentTypeCache contentTypeCache, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public MemberCache(bool previewDefault, ICacheProvider snapshotCache, IMemberService memberService, IDataTypeService dataTypeService, ILocalizationService localizationService, PublishedContentTypeCache contentTypeCache, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { _snapshotCache = snapshotCache; _publishedSnapshotAccessor = publishedSnapshotAccessor; + VariationContextAccessor = variationContextAccessor; _memberService = memberService; _dataTypeService = dataTypeService; _localizationService = localizationService; @@ -63,14 +65,14 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = _memberService.GetById(memberId); return member == null ? null - : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor); + : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); }); } private IPublishedContent /*IPublishedMember*/ GetById(IMember member, bool previewing) { return GetCacheItem(CacheKeys.MemberCacheMember("ById", _previewDefault, member.Id), () => - PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor)); + PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor, VariationContextAccessor)); } public IPublishedContent /*IPublishedMember*/ GetByProviderKey(object key) @@ -105,7 +107,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public IPublishedContent /*IPublishedMember*/ GetByMember(IMember member) { - return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor); + return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); } public IEnumerable GetAtRoot(bool preview) @@ -113,7 +115,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // because members are flat (not a tree) everything is at root // because we're loading everything... let's just not cache? var members = _memberService.GetAllMembers(); - return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor)); + return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor, VariationContextAccessor)); } public XPathNavigator CreateNavigator() diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs index 2357d17273..2fe7a26d45 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable XmlString(i++, _content.TemplateId), XmlString(i++, _content.WriterId), XmlString(i++, _content.CreatorId), - XmlString(i++, _content.UrlName), + XmlString(i++, _content.UrlSegment), XmlString(i, _content.IsDraft) }; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComponent.cs index 3482a6cf2c..1d497d73e0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComponent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComponent.cs @@ -1,11 +1,5 @@ -using Umbraco.Core; -using Umbraco.Core.Components; -using Umbraco.Core.Logging; -using Umbraco.Core.Scoping; -using Umbraco.Core.Services; -using LightInject; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Components; +using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache { @@ -15,6 +9,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { base.Compose(composition); + // register the NuCache database data source + composition.Container.Register(); + // register the NuCache published snapshot service // must register default options, required in the service ctor composition.Container.Register(factory => new PublishedSnapshotService.Options()); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index f2e3355750..6d161dc3b9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Xml.Serialization; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Collections; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -18,7 +19,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly Guid _contentUid; private readonly bool _isPreviewing; private readonly bool _isMember; - private readonly IPublishedContent _content; + private readonly PublishedContent _content; + private readonly ContentVariation _variations; private readonly object _locko = new object(); @@ -34,7 +36,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private CacheValues _cacheValues; private string _valuesCacheKey; - private string _recurseCacheKey; // initializes a published content property with no value public Property(PublishedPropertyType propertyType, PublishedContent content, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) @@ -49,7 +50,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { foreach (var sourceValue in sourceValues) { - if (sourceValue.Culture == null && sourceValue.Segment == null) + if (sourceValue.Culture == "" && sourceValue.Segment == "") { _sourceValue = sourceValue.Value; } @@ -68,10 +69,11 @@ namespace Umbraco.Web.PublishedCache.NuCache _isPreviewing = content.IsPreviewing; _isMember = content.ContentType.ItemType == PublishedItemType.Member; _publishedSnapshotAccessor = publishedSnapshotAccessor; + _variations = propertyType.Variations; } // clone for previewing as draft a published content that is published and has no draft - public Property(Property origin, IPublishedContent content) + public Property(Property origin, PublishedContent content) : base(origin.PropertyType, origin.ReferenceCacheLevel) { _sourceValue = origin._sourceValue; @@ -82,15 +84,12 @@ namespace Umbraco.Web.PublishedCache.NuCache _isPreviewing = true; _isMember = origin._isMember; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; + _variations = origin._variations; } public override bool HasValue(string culture = null, string segment = null) => _sourceValue != null && (!(_sourceValue is string) || string.IsNullOrWhiteSpace((string) _sourceValue) == false); - // used to cache the recursive *property* for this property - internal string RecurseCacheKey => _recurseCacheKey - ?? (_recurseCacheKey = CacheKeys.PropertyRecurse(_contentUid, Alias, _isPreviewing)); - // used to cache the CacheValues of this property internal string ValuesCacheKey => _valuesCacheKey ?? (_valuesCacheKey = CacheKeys.PropertyCacheValues(_contentUid, Alias, _isPreviewing)); @@ -145,7 +144,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is always invoked from within a lock, so does not require its own lock private object GetInterValue(string culture, string segment) { - if (culture == null && segment == null) + if (culture == "" && segment == "") { if (_interInitialized) return _interValue; _interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing); @@ -158,7 +157,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var k = new CompositeStringStringKey(culture, segment); if (!_sourceValues.TryGetValue(k, out var vvalue)) - _sourceValues[k] = vvalue = new SourceInterValue { Culture = culture, Segment = segment }; + _sourceValues[k] = vvalue = new SourceInterValue { Culture = culture, Segment = segment, SourceValue = GetSourceValue(culture, segment) }; if (vvalue.InterInitialized) return vvalue.InterValue; vvalue.InterValue = PropertyType.ConvertSourceToInter(_content, vvalue.SourceValue, _isPreviewing); @@ -168,7 +167,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetSourceValue(string culture = null, string segment = null) { - if (culture == null && segment == null) + ContextualizeVariation(ref culture, ref segment); + + if (culture == "" && segment == "") return _sourceValue; lock (_locko) @@ -178,8 +179,22 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private void ContextualizeVariation(ref string culture, ref string segment) + { + if (culture != null && segment != null) return; + + // use context values + // fixme CultureSegment? + var publishedVariationContext = _content.VariationContextAccessor?.VariationContext; + if (culture == null) culture = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Culture : ""; + if (segment == null) segment = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Segment : ""; + } + public override object GetValue(string culture = null, string segment = null) { + ContextualizeVariation(ref culture, ref segment); + + object value; lock (_locko) { var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); @@ -190,12 +205,16 @@ namespace Umbraco.Web.PublishedCache.NuCache if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); cacheValues.ObjectInitialized = true; - return cacheValues.ObjectValue; + value = cacheValues.ObjectValue; } + + return value; } public override object GetXPathValue(string culture = null, string segment = null) { + ContextualizeVariation(ref culture, ref segment); + lock (_locko) { var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); @@ -227,7 +246,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is always invoked from within a lock, so does not require its own lock public CacheValue For(string culture, string segment) { - if (culture == null && segment == null) + if (culture == "" && segment == "") return this; if (_values == null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index b5201716ac..d944c5f824 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -18,18 +18,18 @@ namespace Umbraco.Web.PublishedCache.NuCache // ReSharper disable once InconsistentNaming internal readonly ContentData _contentData; // internal for ContentNode cloning - private readonly string _urlName; - private IReadOnlyDictionary _cultureNames; + private readonly string _urlSegment; #region Constructors - public PublishedContent(ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public PublishedContent(ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { _contentNode = contentNode; _contentData = contentData; _publishedSnapshotAccessor = publishedSnapshotAccessor; + VariationContextAccessor = variationContextAccessor; - _urlName = _contentData.Name.ToUrlSegment(); + _urlSegment = _contentData.Name.ToUrlSegment(); IsPreviewing = _contentData.Published == false; var properties = new List(); @@ -70,9 +70,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { _contentNode = contentNode; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; + VariationContextAccessor = origin.VariationContextAccessor; _contentData = origin._contentData; - _urlName = origin._urlName; + _urlSegment = origin._urlSegment; IsPreviewing = origin.IsPreviewing; // here is the main benefit: we do not re-create properties so if anything @@ -85,10 +86,11 @@ namespace Umbraco.Web.PublishedCache.NuCache private PublishedContent(PublishedContent origin) { _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; + VariationContextAccessor = origin.VariationContextAccessor; _contentNode = origin._contentNode; _contentData = origin._contentData; - _urlName = origin._urlName; + _urlSegment = origin._urlSegment; IsPreviewing = true; // clone properties so _isPreviewing is true @@ -152,51 +154,133 @@ namespace Umbraco.Web.PublishedCache.NuCache #endregion - #region IPublishedContent + #region Content Type - public override int Id => _contentNode.Id; + /// + public override PublishedContentType ContentType => _contentNode.ContentType; + + #endregion + + #region PublishedElement + + /// public override Guid Key => _contentNode.Uid; - public override int DocumentTypeId => _contentNode.ContentType.Id; - public override string DocumentTypeAlias => _contentNode.ContentType.Alias; - public override PublishedItemType ItemType => _contentNode.ContentType.ItemType; - public override string Name => _contentData.Name; - public override IReadOnlyDictionary CultureNames + #endregion + + #region PublishedContent + + /// + public override int Id => _contentNode.Id; + + /// + public override string Name { get { - if (!ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - return null; + if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + return _contentData.Name; - if (_cultureNames == null) - { - var d = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach(var c in _contentData.CultureInfos) - { - d[c.Key] = new PublishedCultureName(c.Value.Name, c.Value.Name.ToUrlSegment()); - } - _cultureNames = d; - } - return _cultureNames; + var culture = VariationContextAccessor.VariationContext.Culture; + if (culture == "") + return _contentData.Name; + + return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.Name : null; } } - public override int Level => _contentNode.Level; - public override string Path => _contentNode.Path; + + /// + public override string UrlSegment + { + get + { + if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + return _urlSegment; + + var culture = VariationContextAccessor.VariationContext.Culture; + if (culture == "") + return _urlSegment; + + return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.UrlSegment : null; + } + } + + /// public override int SortOrder => _contentNode.SortOrder; + + /// + public override int Level => _contentNode.Level; + + /// + public override string Path => _contentNode.Path; + + /// public override int TemplateId => _contentData.TemplateId; - public override string UrlName => _urlName; - - public override DateTime CreateDate => _contentNode.CreateDate; - public override DateTime UpdateDate => _contentData.VersionDate; - + /// public override int CreatorId => _contentNode.CreatorId; + + /// public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); + + /// + public override DateTime CreateDate => _contentNode.CreateDate; + + /// public override int WriterId => _contentData.WriterId; + + /// public override string WriterName => GetProfileNameById(_contentData.WriterId); + /// + public override DateTime UpdateDate => _contentData.VersionDate; + + private IReadOnlyDictionary _cultureInfos; + + private static readonly IReadOnlyDictionary NoCultureInfos = new Dictionary(); + + /// + public override PublishedCultureInfos GetCulture(string culture = null) + { + // handle context culture + if (culture == null) + culture = VariationContextAccessor.VariationContext.Culture; + + // no invariant culture infos + if (culture == "") return null; + + // get + return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; + } + + /// + public override IReadOnlyDictionary Cultures + { + get + { + if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + return NoCultureInfos; + + if (_cultureInfos != null) return _cultureInfos; + + if (_contentData.CultureInfos == null) + throw new Exception("oops: _contentDate.CultureInfos is null."); + return _cultureInfos = _contentData.CultureInfos + .ToDictionary(x => x.Key, x => new PublishedCultureInfos(x.Key, x.Value.Name, x.Value.Date)); + } + } + + /// + public override PublishedItemType ItemType => _contentNode.ContentType.ItemType; + + /// public override bool IsDraft => _contentData.Published == false; + #endregion + + #region Tree + + /// public override IPublishedContent Parent { get @@ -215,10 +299,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - private string _childrenCacheKey; - - private string ChildrenCacheKey => _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, IsPreviewing)); - + /// public override IEnumerable Children { get @@ -232,6 +313,10 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private string _childrenCacheKey; + + private string ChildrenCacheKey => _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, IsPreviewing)); + private IEnumerable GetChildren() { IEnumerable c; @@ -255,8 +340,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // Q: perfs-wise, is it better than having the store managed an ordered list } + #endregion + + #region Properties + + + /// public override IEnumerable Properties => PropertiesArray; + /// public override IPublishedProperty GetProperty(string alias) { var index = _contentNode.ContentType.GetPropertyIndex(alias); @@ -267,21 +359,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return property; } - public override IPublishedProperty GetProperty(string alias, bool recurse) - { - var property = GetProperty(alias); - if (recurse == false) return property; - - var cache = GetAppropriateCache(); - if (cache == null) - return base.GetProperty(alias, true); - - var key = ((Property)property).RecurseCacheKey; - return (Property)cache.GetCacheItem(key, () => base.GetProperty(alias, true)); - } - - public override PublishedContentType ContentType => _contentNode.ContentType; - #endregion #region Caching @@ -308,6 +385,9 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Internal + // used by property + internal IVariationContextAccessor VariationContextAccessor { get; } + // used by navigable content internal IPublishedProperty[] PropertiesArray { get; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs index 8b3d01f6e9..47c8d738f1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs @@ -15,13 +15,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly IMember _member; - private PublishedMember(IMember member, ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor) - : base(contentNode, contentData, publishedSnapshotAccessor) + private PublishedMember(IMember member, ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) + : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor) { _member = member; } - public static IPublishedContent Create(IMember member, PublishedContentType contentType, bool previewing, IPublishedSnapshotAccessor publishedSnapshotAccessor) + public static IPublishedContent Create(IMember member, PublishedContentType contentType, bool previewing, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { var d = new ContentData { @@ -37,7 +37,7 @@ namespace Umbraco.Web.PublishedCache.NuCache member.Level, member.Path, member.SortOrder, member.ParentId, member.CreateDate, member.CreatorId); - return new PublishedMember(member, n, d, publishedSnapshotAccessor).CreateModel(); + return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); } private static Dictionary GetPropertyValues(PublishedContentType contentType, IMember member) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index 741e4d09f5..9b8982c69c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -17,7 +17,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { _service = service; _defaultPreview = defaultPreview; - SnapshotCache = new ObjectCacheRuntimeCacheProvider(); } public class PublishedSnapshotElements : IDisposable @@ -49,7 +48,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Caches - public ICacheProvider SnapshotCache { get; } + public ICacheProvider SnapshotCache => Elements.SnapshotCache; public ICacheProvider ElementsCache => Elements.ElementsCache; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 5a128855fb..68812b8e4e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -28,7 +28,6 @@ using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Routing; -using Database = Umbraco.Web.PublishedCache.NuCache.DataSource.Database; namespace Umbraco.Web.PublishedCache.NuCache { @@ -37,14 +36,14 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; private readonly IScopeProvider _scopeProvider; - private readonly Database _dataSource; + private readonly IDataSource _dataSource; private readonly ILogger _logger; private readonly IDocumentRepository _documentRepository; private readonly IMediaRepository _mediaRepository; private readonly IMemberRepository _memberRepository; private readonly IGlobalSettings _globalSettings; private readonly ISiteDomainHelper _siteDomainHelper; - private readonly ISystemDefaultCultureProvider _systemDefaultCultureProvider; + private readonly IDefaultCultureAccessor _defaultCultureAccessor; // volatile because we read it with no lock private volatile bool _isReady; @@ -82,24 +81,25 @@ namespace Umbraco.Web.PublishedCache.NuCache public PublishedSnapshotService(Options options, MainDom mainDom, IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap, - IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger, IScopeProvider scopeProvider, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, + ILogger logger, IScopeProvider scopeProvider, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, - ISystemDefaultCultureProvider systemDefaultCultureProvider, - IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper) - : base(publishedSnapshotAccessor) + IDefaultCultureAccessor defaultCultureAccessor, + IDataSource dataSource, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper) + : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) // throw new Exception("Singleton must be instancianted only once!"); _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; - _dataSource = new Database(); + _dataSource = dataSource; _logger = logger; _scopeProvider = scopeProvider; _documentRepository = documentRepository; _mediaRepository = mediaRepository; _memberRepository = memberRepository; - _systemDefaultCultureProvider = systemDefaultCultureProvider; + _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings; _siteDomainHelper = siteDomainHelper; @@ -145,13 +145,13 @@ namespace Umbraco.Web.PublishedCache.NuCache // stores are created with a db so they can write to it, but they do not read from it, // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to // figure out whether it can read the dbs or it should populate them from sql - _contentStore = new ContentStore(publishedSnapshotAccessor, logger, _localContentDb); - _mediaStore = new ContentStore(publishedSnapshotAccessor, logger, _localMediaDb); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); } else { - _contentStore = new ContentStore(publishedSnapshotAccessor, logger); - _mediaStore = new ContentStore(publishedSnapshotAccessor, logger); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); } _domainStore = new SnapDictionary(); @@ -173,7 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { - if (_localDbExists) // fixme? + if (_localDbExists) { LockAndLoadContent(LoadContentFromLocalDbLocked); LockAndLoadMedia(LoadMediaFromLocalDbLocked); @@ -333,7 +333,6 @@ namespace Umbraco.Web.PublishedCache.NuCache //private void LoadContent(IContent content) //{ // var contentService = _serviceContext.ContentService as ContentService; - // if (contentService == null) throw new Exception("oops"); // var newest = content; // var published = newest.Published // ? newest @@ -536,7 +535,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } if (draftChanged || publishedChanged) - ((PublishedSnapshot)CurrentPublishedSnapshot).Resync(); + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) @@ -544,9 +543,6 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = false; draftChanged = false; - if (!(_serviceContext.ContentService is ContentService)) - throw new Exception("oops"); - // locks: // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) @@ -633,16 +629,13 @@ namespace Umbraco.Web.PublishedCache.NuCache } if (anythingChanged) - ((PublishedSnapshot)CurrentPublishedSnapshot).Resync(); + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } private void NotifyLocked(IEnumerable payloads, out bool anythingChanged) { anythingChanged = false; - if (!(_serviceContext.MediaService is MediaService)) - throw new Exception("oops"); - // locks: // see notes for content cache refresher @@ -722,7 +715,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); - ((PublishedSnapshot)CurrentPublishedSnapshot).Resync(); + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, IEnumerable, IEnumerable, IEnumerable> action) @@ -778,9 +771,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // some locking on datatypes _publishedContentTypeFactory.NotifyDataTypeChanges(idsA); - if (!(_serviceContext.ContentService is ContentService)) - throw new Exception("oops"); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); @@ -788,9 +778,6 @@ namespace Umbraco.Web.PublishedCache.NuCache scope.Complete(); } - if (!(_serviceContext.MediaService is MediaService)) - throw new Exception("oops"); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); @@ -799,7 +786,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - ((PublishedSnapshot)CurrentPublishedSnapshot).Resync(); + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } public override void Notify(DomainCacheRefresher.JsonPayload[] payloads) @@ -897,9 +884,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - if (!(_serviceContext.ContentService is ContentService)) - throw new Exception("oops"); - var refreshedIdsA = refreshedIds.ToArray(); using (var scope = _scopeProvider.CreateScope()) @@ -921,9 +905,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // mediaStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - if (!(_serviceContext.MediaService is MediaService)) - throw new Exception("oops"); - var refreshedIdsA = refreshedIds.ToArray(); using (var scope = _scopeProvider.CreateScope()) @@ -955,6 +936,9 @@ namespace Umbraco.Web.PublishedCache.NuCache return new PublishedSnapshot(this, preview); } + // gets a new set of elements + // always creates a new set of elements, + // even though the underlying elements may not change (store snapshots) public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault) { // note: using ObjectCacheRuntimeCacheProvider for elements and snapshot caches @@ -996,7 +980,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // a MaxValue to make sure this one runs last, and it should be ok scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { - ((PublishedSnapshot)svc.CurrentPublishedSnapshot).Resync(); + ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); }, int.MaxValue); } @@ -1014,7 +998,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var memberTypeCache = new PublishedContentTypeCache(null, null, _serviceContext.MemberTypeService, _publishedContentTypeFactory, _logger); - var defaultCulture = _systemDefaultCultureProvider.DefaultCulture; + var defaultCulture = _defaultCultureAccessor.DefaultCulture; var domainCache = new DomainCache(domainSnap, defaultCulture); var domainHelper = new DomainHelper(domainCache, _siteDomainHelper); @@ -1022,7 +1006,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainHelper, _globalSettings, _serviceContext.LocalizationService), MediaCache = new MediaCache(previewDefault, mediaSnap, snapshotCache, elementsCache), - MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, _serviceContext.DataTypeService, _serviceContext.LocalizationService, memberTypeCache, PublishedSnapshotAccessor), + MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, _serviceContext.DataTypeService, _serviceContext.LocalizationService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor), DomainCache = domainCache, SnapshotCache = snapshotCache, ElementsCache = elementsCache @@ -1080,21 +1064,6 @@ namespace Umbraco.Web.PublishedCache.NuCache db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = item.Id }); } - private static readonly string[] PropertiesImpactingAllVersions = { "SortOrder", "ParentId", "Level", "Path", "Trashed" }; - - private static bool HasChangesImpactingAllVersions(IContent icontent) - { - var content = (Content)icontent; - - // UpdateDate will be dirty - // Published may be dirty if saving a Published entity - // so cannot do this (would always be true): - //return content.IsEntityDirty(); - - // have to be more precise & specify properties - return PropertiesImpactingAllVersions.Any(content.IsPropertyDirty); - } - private void OnContentRefreshedEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args) { var db = args.Scope.Database; @@ -1216,7 +1185,6 @@ namespace Umbraco.Web.PublishedCache.NuCache var cultureData = new Dictionary(); - // fixme refactor!!! var names = content is IContent document ? (published ? document.PublishNames @@ -1225,7 +1193,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name }; + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) }; } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index bf764b0ff0..2064bf51ea 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -87,15 +87,6 @@ namespace Umbraco.Web.PublishedCache public override IEnumerable Properties => _properties; - public override IPublishedProperty GetProperty(string alias, bool recurse) - { - if (recurse) - { - throw new NotSupportedException(); - } - return GetProperty(alias); - } - public override IPublishedProperty GetProperty(string alias) { return _properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); @@ -144,13 +135,11 @@ namespace Umbraco.Web.PublishedCache public override string Name => _member.Name; - public override IReadOnlyDictionary CultureNames => throw new NotSupportedException(); + public override PublishedCultureInfos GetCulture(string culture = null) => throw new NotSupportedException(); - public override string UrlName => throw new NotSupportedException(); + public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); - public override string DocumentTypeAlias => _member.ContentTypeAlias; - - public override int DocumentTypeId => _member.ContentType.Id; + public override string UrlSegment => throw new NotSupportedException(); //TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current public override string WriterName => _member.GetCreatorProfile().Name; diff --git a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs index 685c129224..74e08e92e8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs @@ -1,17 +1,20 @@ using System.Collections.Generic; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache { abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService { - protected PublishedSnapshotServiceBase(IPublishedSnapshotAccessor publishedSnapshotAccessor) + protected PublishedSnapshotServiceBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { PublishedSnapshotAccessor = publishedSnapshotAccessor; + VariationContextAccessor = variationContextAccessor; } public IPublishedSnapshotAccessor PublishedSnapshotAccessor { get; } + public IVariationContextAccessor VariationContextAccessor { get; } // note: NOT setting _publishedSnapshotAccessor.PublishedSnapshot here because it is the // responsibility of the caller to manage what the 'current' facade is diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs new file mode 100644 index 0000000000..5cd9dd0069 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Xml.XPath; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Composing; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// An IPublishedContent that is represented all by a dictionary. + /// + /// + /// This is a helper class and definitely not intended for public use, it expects that all of the values required + /// to create an IPublishedContent exist in the dictionary by specific aliases. + /// + internal class DictionaryPublishedContent : PublishedContentBase + { + // note: I'm not sure this class fully complies with IPublishedContent rules especially + // I'm not sure that _properties contains all properties including those without a value, + // neither that GetProperty will return a property without a value vs. null... @zpqrtbnk + + // List of properties that will appear in the XML and do not match + // anything in the ContentType, so they must be ignored. + private static readonly string[] IgnoredKeys = { "version", "isDoc" }; + + public DictionaryPublishedContent( + IReadOnlyDictionary valueDictionary, + Func getParent, + Func> getChildren, + Func getProperty, + ICacheProvider cacheProvider, + PublishedContentTypeCache contentTypeCache, + XPathNavigator nav, + bool fromExamine) + { + if (valueDictionary == null) throw new ArgumentNullException(nameof(valueDictionary)); + if (getParent == null) throw new ArgumentNullException(nameof(getParent)); + if (getProperty == null) throw new ArgumentNullException(nameof(getProperty)); + + _getParent = new Lazy(() => getParent(ParentId)); + _getChildren = new Lazy>(() => getChildren(Id, nav)); + _getProperty = getProperty; + _cacheProvider = cacheProvider; + + LoadedFromExamine = fromExamine; + + ValidateAndSetProperty(valueDictionary, val => _id = Int32.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! + ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); + //ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); + ValidateAndSetProperty(valueDictionary, val => _sortOrder = Int32.Parse(val), "sortOrder"); + ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName", "__nodeName"); + ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName"); + ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", LuceneIndexer.ItemTypeFieldName); + ValidateAndSetProperty(valueDictionary, val => _documentTypeId = Int32.Parse(val), "nodeType"); + //ValidateAndSetProperty(valueDictionary, val => _writerName = val, "writerName"); + ValidateAndSetProperty(valueDictionary, val => _creatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132 + //ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID"); + ValidateAndSetProperty(valueDictionary, val => _creatorId = Int32.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132 + ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path"); + ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate"); + ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate"); + ValidateAndSetProperty(valueDictionary, val => _level = Int32.Parse(val), "level"); + ValidateAndSetProperty(valueDictionary, val => + { + int pId; + ParentId = -1; + if (Int32.TryParse(val, out pId)) + { + ParentId = pId; + } + }, "parentID"); + + _contentType = contentTypeCache.Get(PublishedItemType.Media, _documentTypeAlias); + _properties = new Collection(); + + //handle content type properties + //make sure we create them even if there's no value + foreach (var propertyType in _contentType.PropertyTypes) + { + var alias = propertyType.Alias; + _keysAdded.Add(alias); + string value; + const bool isPreviewing = false; // false :: never preview a media + var property = valueDictionary.TryGetValue(alias, out value) == false || value == null + ? new XmlPublishedProperty(propertyType, this, isPreviewing) + : new XmlPublishedProperty(propertyType, this, isPreviewing, value); + _properties.Add(property); + } + + //loop through remaining values that haven't been applied + foreach (var i in valueDictionary.Where(x => + _keysAdded.Contains(x.Key) == false // not already processed + && IgnoredKeys.Contains(x.Key) == false)) // not ignorable + { + if (i.Key.InvariantStartsWith("__")) + { + // no type for that one, dunno how to convert, drop it + //IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty); + //_properties.Add(property); + } + else + { + // this is a property that does not correspond to anything, ignore and log + Current.Logger.Warn("Dropping property \"" + i.Key + "\" because it does not belong to the content type."); + } + } + } + + private DateTime ParseDateTimeValue(string val) + { + if (LoadedFromExamine == false) + return DateTime.Parse(val); + + //we need to parse the date time using Lucene converters + var ticks = Int64.Parse(val); + return new DateTime(ticks); + } + + /// + /// Flag to get/set if this was laoded from examine cache + /// + internal bool LoadedFromExamine { get; } + + //private readonly Func _getParent; + private readonly Lazy _getParent; + //private readonly Func> _getChildren; + private readonly Lazy> _getChildren; + private readonly Func _getProperty; + private readonly ICacheProvider _cacheProvider; + + /// + /// Returns 'Media' as the item type + /// + public override PublishedItemType ItemType => PublishedItemType.Media; + + public override IPublishedContent Parent => _getParent.Value; + + public int ParentId { get; private set; } + + public override int Id => _id; + + public override Guid Key => _key; + + public override int TemplateId => 0; + + public override int SortOrder => _sortOrder; + + public override string Name => _name; + + public override PublishedCultureInfos GetCulture(string culture = null) => throw new NotSupportedException(); + + public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); + + public override string UrlSegment => _urlName; + + public override string WriterName => _creatorName; + + public override string CreatorName => _creatorName; + + public override int WriterId => _creatorId; + + public override int CreatorId => _creatorId; + + public override string Path => _path; + + public override DateTime CreateDate => _createDate; + + public override DateTime UpdateDate => _updateDate; + + public override int Level => _level; + + public override bool IsDraft => false; + + public override IEnumerable Properties => _properties; + + public override IEnumerable Children => _getChildren.Value; + + public override IPublishedProperty GetProperty(string alias) + { + return _getProperty(this, alias); + } + + public override PublishedContentType ContentType => _contentType; + + private readonly List _keysAdded = new List(); + private int _id; + private Guid _key; + //private int _templateId; + private int _sortOrder; + private string _name; + private string _urlName; + private string _documentTypeAlias; + private int _documentTypeId; + //private string _writerName; + private string _creatorName; + //private int _writerId; + private int _creatorId; + private string _path; + private DateTime _createDate; + private DateTime _updateDate; + //private Guid _version; + private int _level; + private readonly ICollection _properties; + private readonly PublishedContentType _contentType; + + private void ValidateAndSetProperty(IReadOnlyDictionary valueDictionary, Action setProperty, params string[] potentialKeys) + { + var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null); + if (key == null) + { + throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + String.Join(",", potentialKeys) + "' elements"); + } + + setProperty(valueDictionary[key]); + _keysAdded.Add(key); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs index 0f40d78225..a64dbb7916 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs @@ -11,10 +11,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { private readonly IDomainService _domainService; - public DomainCache(IDomainService domainService, ISystemDefaultCultureProvider systemDefaultCultureProvider) + public DomainCache(IDomainService domainService, IDefaultCultureAccessor defaultCultureAccessor) { _domainService = domainService; - DefaultCulture = systemDefaultCultureProvider.DefaultCulture; + DefaultCulture = defaultCultureAccessor.DefaultCulture; } /// diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 57d9e0a980..7aac1ed017 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -266,7 +266,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache while (hasDomains == false && n != null) // n is null at root { // get the url - var urlName = n.UrlName; + var urlName = n.UrlSegment; pathParts.Add(urlName); // move to parent node diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index 48a63aa8d3..8f354240e0 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Configuration; using System.IO; using System.Linq; using System.Threading; using System.Xml.XPath; using Examine; -using Examine.LuceneEngine.Providers; using Examine.LuceneEngine.SearchCriteria; using Examine.Providers; using Lucene.Net.Store; @@ -16,7 +14,6 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; -using Umbraco.Web.Models; using Umbraco.Examine; using umbraco; using Umbraco.Core.Cache; @@ -607,230 +604,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return mediaList; } - /// - /// An IPublishedContent that is represented all by a dictionary. - /// - /// - /// This is a helper class and definitely not intended for public use, it expects that all of the values required - /// to create an IPublishedContent exist in the dictionary by specific aliases. - /// - internal class DictionaryPublishedContent : PublishedContentBase - { - // note: I'm not sure this class fully complies with IPublishedContent rules especially - // I'm not sure that _properties contains all properties including those without a value, - // neither that GetProperty will return a property without a value vs. null... @zpqrtbnk - - // List of properties that will appear in the XML and do not match - // anything in the ContentType, so they must be ignored. - private static readonly string[] IgnoredKeys = { "version", "isDoc" }; - - public DictionaryPublishedContent( - IReadOnlyDictionary valueDictionary, - Func getParent, - Func> getChildren, - Func getProperty, - ICacheProvider cacheProvider, - PublishedContentTypeCache contentTypeCache, - XPathNavigator nav, - bool fromExamine) - { - if (valueDictionary == null) throw new ArgumentNullException(nameof(valueDictionary)); - if (getParent == null) throw new ArgumentNullException(nameof(getParent)); - if (getProperty == null) throw new ArgumentNullException(nameof(getProperty)); - - _getParent = new Lazy(() => getParent(ParentId)); - _getChildren = new Lazy>(() => getChildren(Id, nav)); - _getProperty = getProperty; - _cacheProvider = cacheProvider; - - LoadedFromExamine = fromExamine; - - ValidateAndSetProperty(valueDictionary, val => _id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! - ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); - //ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); - ValidateAndSetProperty(valueDictionary, val => _sortOrder = int.Parse(val), "sortOrder"); - ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName", "__nodeName"); - ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName"); - ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", LuceneIndexer.ItemTypeFieldName); - ValidateAndSetProperty(valueDictionary, val => _documentTypeId = int.Parse(val), "nodeType"); - //ValidateAndSetProperty(valueDictionary, val => _writerName = val, "writerName"); - ValidateAndSetProperty(valueDictionary, val => _creatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132 - //ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID"); - ValidateAndSetProperty(valueDictionary, val => _creatorId = int.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132 - ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path"); - ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate"); - ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate"); - ValidateAndSetProperty(valueDictionary, val => _level = int.Parse(val), "level"); - ValidateAndSetProperty(valueDictionary, val => - { - int pId; - ParentId = -1; - if (int.TryParse(val, out pId)) - { - ParentId = pId; - } - }, "parentID"); - - _contentType = contentTypeCache.Get(PublishedItemType.Media, _documentTypeAlias); - _properties = new Collection(); - - //handle content type properties - //make sure we create them even if there's no value - foreach (var propertyType in _contentType.PropertyTypes) - { - var alias = propertyType.Alias; - _keysAdded.Add(alias); - string value; - const bool isPreviewing = false; // false :: never preview a media - var property = valueDictionary.TryGetValue(alias, out value) == false || value == null - ? new XmlPublishedProperty(propertyType, this, isPreviewing) - : new XmlPublishedProperty(propertyType, this, isPreviewing, value); - _properties.Add(property); - } - - //loop through remaining values that haven't been applied - foreach (var i in valueDictionary.Where(x => - _keysAdded.Contains(x.Key) == false // not already processed - && IgnoredKeys.Contains(x.Key) == false)) // not ignorable - { - if (i.Key.InvariantStartsWith("__")) - { - // no type for that one, dunno how to convert, drop it - //IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty); - //_properties.Add(property); - } - else - { - // this is a property that does not correspond to anything, ignore and log - Current.Logger.Warn("Dropping property \"" + i.Key + "\" because it does not belong to the content type."); - } - } - } - - private DateTime ParseDateTimeValue(string val) - { - if (LoadedFromExamine == false) - return DateTime.Parse(val); - - //we need to parse the date time using Lucene converters - var ticks = long.Parse(val); - return new DateTime(ticks); - } - - /// - /// Flag to get/set if this was laoded from examine cache - /// - internal bool LoadedFromExamine { get; } - - //private readonly Func _getParent; - private readonly Lazy _getParent; - //private readonly Func> _getChildren; - private readonly Lazy> _getChildren; - private readonly Func _getProperty; - private readonly ICacheProvider _cacheProvider; - - /// - /// Returns 'Media' as the item type - /// - public override PublishedItemType ItemType => PublishedItemType.Media; - - public override IPublishedContent Parent => _getParent.Value; - - public int ParentId { get; private set; } - - public override int Id => _id; - - public override Guid Key => _key; - - public override int TemplateId => 0; - - public override int SortOrder => _sortOrder; - - public override string Name => _name; - - public override IReadOnlyDictionary CultureNames => throw new NotSupportedException(); - - public override string UrlName => _urlName; - - public override string DocumentTypeAlias => _documentTypeAlias; - - public override int DocumentTypeId => _documentTypeId; - - public override string WriterName => _creatorName; - - public override string CreatorName => _creatorName; - - public override int WriterId => _creatorId; - - public override int CreatorId => _creatorId; - - public override string Path => _path; - - public override DateTime CreateDate => _createDate; - - public override DateTime UpdateDate => _updateDate; - - public override int Level => _level; - - public override bool IsDraft => false; - - public override IEnumerable Properties => _properties; - - public override IEnumerable Children => _getChildren.Value; - - public override IPublishedProperty GetProperty(string alias) - { - return _getProperty(this, alias); - } - - public override PublishedContentType ContentType => _contentType; - - // override to implement cache - // cache at context level, ie once for the whole request - // but cache is not shared by requests because we wouldn't know how to clear it - public override IPublishedProperty GetProperty(string alias, bool recurse) - { - if (recurse == false) return GetProperty(alias); - - var key = $"XmlPublishedCache.PublishedMediaCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - var cacheProvider = _cacheProvider; - return cacheProvider.GetCacheItem(key, () => base.GetProperty(alias, true)); - } - - private readonly List _keysAdded = new List(); - private int _id; - private Guid _key; - //private int _templateId; - private int _sortOrder; - private string _name; - private string _urlName; - private string _documentTypeAlias; - private int _documentTypeId; - //private string _writerName; - private string _creatorName; - //private int _writerId; - private int _creatorId; - private string _path; - private DateTime _createDate; - private DateTime _updateDate; - //private Guid _version; - private int _level; - private readonly ICollection _properties; - private readonly PublishedContentType _contentType; - - private void ValidateAndSetProperty(IReadOnlyDictionary valueDictionary, Action setProperty, params string[] potentialKeys) - { - var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null); - if (key == null) - { - throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + string.Join(",", potentialKeys) + "' elements"); - } - - setProperty(valueDictionary[key]); - _keysAdded.Add(key); - } - } - internal void Resync() { // clear recursive properties cached by XmlPublishedContent.GetProperty diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedSnapshotService.cs index 3b9a97d8cc..78585ba2e2 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedSnapshotService.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private readonly IUserService _userService; private readonly ICacheProvider _requestCache; private readonly IGlobalSettings _globalSettings; - private readonly ISystemDefaultCultureProvider _systemDefaultCultureProvider; + private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly ISiteDomainHelper _siteDomainHelper; #region Constructors @@ -43,17 +43,18 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache IScopeProvider scopeProvider, ICacheProvider requestCache, IEnumerable segmentProviders, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, - ISystemDefaultCultureProvider systemDefaultCultureProvider, + IDefaultCultureAccessor defaultCultureAccessor, ILogger logger, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper, MainDom mainDom, bool testing = false, bool enableRepositoryEvents = true) - : this(serviceContext, publishedContentTypeFactory, scopeProvider, requestCache, segmentProviders, publishedSnapshotAccessor, + : this(serviceContext, publishedContentTypeFactory, scopeProvider, requestCache, segmentProviders, + publishedSnapshotAccessor, variationContextAccessor, documentRepository, mediaRepository, memberRepository, - systemDefaultCultureProvider, + defaultCultureAccessor, logger, globalSettings, siteDomainHelper, null, mainDom, testing, enableRepositoryEvents) { } @@ -62,18 +63,19 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache IPublishedContentTypeFactory publishedContentTypeFactory, IScopeProvider scopeProvider, ICacheProvider requestCache, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, - ISystemDefaultCultureProvider systemDefaultCultureProvider, + IDefaultCultureAccessor defaultCultureAccessor, ILogger logger, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper, PublishedContentTypeCache contentTypeCache, MainDom mainDom, bool testing, bool enableRepositoryEvents) - : this(serviceContext, publishedContentTypeFactory, scopeProvider, requestCache, Enumerable.Empty(), publishedSnapshotAccessor, + : this(serviceContext, publishedContentTypeFactory, scopeProvider, requestCache, Enumerable.Empty(), + publishedSnapshotAccessor, variationContextAccessor, documentRepository, mediaRepository, memberRepository, - systemDefaultCultureProvider, + defaultCultureAccessor, logger, globalSettings, siteDomainHelper, contentTypeCache, mainDom, testing, enableRepositoryEvents) { } @@ -82,16 +84,16 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache IScopeProvider scopeProvider, ICacheProvider requestCache, IEnumerable segmentProviders, - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, - ISystemDefaultCultureProvider systemDefaultCultureProvider, + IDefaultCultureAccessor defaultCultureAccessor, ILogger logger, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper, PublishedContentTypeCache contentTypeCache, MainDom mainDom, bool testing, bool enableRepositoryEvents) - : base(publishedSnapshotAccessor) + : base(publishedSnapshotAccessor, variationContextAccessor) { _routesCache = new RoutesCache(); _publishedContentTypeFactory = publishedContentTypeFactory; @@ -106,7 +108,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache _memberService = serviceContext.MemberService; _mediaService = serviceContext.MediaService; _userService = serviceContext.UserService; - _systemDefaultCultureProvider = systemDefaultCultureProvider; + _defaultCultureAccessor = defaultCultureAccessor; _requestCache = requestCache; _globalSettings = globalSettings; @@ -151,7 +153,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // the current caches, but that would mean creating an extra cache (StaticCache // probably) so better use RequestCache. - var domainCache = new DomainCache(_domainService, _systemDefaultCultureProvider); + var domainCache = new DomainCache(_domainService, _defaultCultureAccessor); return new PublishedSnapshot( new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _siteDomainHelper, _contentTypeCache, _routesCache, previewToken), diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheComponent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheComponent.cs index c9794eb99a..496818ab28 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheComponent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheComponent.cs @@ -29,10 +29,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache factory.GetInstance().RequestCache, factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), - factory.GetInstance(), + factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs index 6f9746c57d..2535f61996 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs @@ -79,20 +79,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return _properties.TryGetValue(alias, out property) ? property : null; } - // override to implement cache - // cache at context level, ie once for the whole request - // but cache is not shared by requests because we wouldn't know how to clear it - public override IPublishedProperty GetProperty(string alias, bool recurse) - { - if (recurse == false) return GetProperty(alias); - - var key = $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - var cacheProvider = _cacheProvider; - return cacheProvider.GetCacheItem(key, () => base.GetProperty(alias, true)); - - // note: cleared by PublishedContentCache.Resync - any change here must be applied there - } - public override PublishedItemType ItemType => PublishedItemType.Content; public override IPublishedContent Parent @@ -150,25 +136,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override IReadOnlyDictionary CultureNames => throw new NotSupportedException(); + public override PublishedCultureInfos GetCulture(string culture = null) => throw new NotSupportedException(); - public override string DocumentTypeAlias - { - get - { - if (_nodeInitialized == false) InitializeNode(); - return _docTypeAlias; - } - } - - public override int DocumentTypeId - { - get - { - if (_nodeInitialized == false) InitializeNode(); - return _docTypeId; - } - } + public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); public override string WriterName { @@ -233,7 +203,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override string UrlName + public override string UrlSegment { get { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index a33b8f283a..7708abc57b 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Globalization; using System.Linq; using System.Web; using Examine; @@ -21,6 +20,10 @@ namespace Umbraco.Web /// public static class PublishedContentExtensions { + // see notes in PublishedElementExtensions + // + private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; + #region Urls /// @@ -85,6 +88,8 @@ namespace Umbraco.Web #endregion + // fixme - .HasValue() and .Value() refactoring - in progress - see exceptions below + #region HasValue /// @@ -97,8 +102,10 @@ namespace Umbraco.Web /// Returns true if GetProperty(alias, recurse) is not null and GetProperty(alias, recurse).HasValue is true. public static bool HasValue(this IPublishedContent content, string alias, bool recurse) { - var prop = content.GetProperty(alias, recurse); - return prop != null && prop.HasValue(); + throw new NotImplementedException("WorkInProgress"); + + //var prop = content.GetProperty(alias, recurse); + //return prop != null && prop.HasValue(); } /// @@ -114,40 +121,24 @@ namespace Umbraco.Web public static IHtmlString HasValue(this IPublishedContent content, string alias, bool recurse, string valueIfTrue, string valueIfFalse = null) { - return content.HasValue(alias, recurse) - ? new HtmlString(valueIfTrue) - : new HtmlString(valueIfFalse ?? string.Empty); + throw new NotImplementedException("WorkInProgress"); + + //return content.HasValue(alias, recurse) + // ? new HtmlString(valueIfTrue) + // : new HtmlString(valueIfFalse ?? string.Empty); } #endregion #region Value - /// - /// Recursively gets the value of a content's property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether to recurse. - /// The recursive value of the content's property identified by the alias. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns null. - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedContent content, string alias, bool recurse) - { - var property = content.GetProperty(alias, recurse); - return property?.GetValue(); - } - /// /// Recursively the value of a content's property identified by its alias, if it exists, otherwise a default value. /// /// The content. /// The property alias. + /// The variation language. + /// The variation segment. /// A value indicating whether to recurse. /// The default value. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. @@ -158,10 +149,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedContent content, string alias, bool recurse, object defaultValue) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false) { - var property = content.GetProperty(alias, recurse); - return property == null || property.HasValue() == false ? defaultValue : property.GetValue(); + var property = content.GetProperty(alias); + + if (property != null && property.HasValue(culture, segment)) + return property.GetValue(culture, segment); + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); } #endregion @@ -174,6 +169,9 @@ namespace Umbraco.Web /// The target property type. /// The content. /// The property alias. + /// The variation language. + /// The variation segment. + /// The default value. /// A value indicating whether to recurse. /// The value of the content's property identified by the alias, converted to the specified type. /// @@ -183,38 +181,37 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, bool recurse) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false) { - return content.Value(alias, recurse, false, default(T)); + var property = content.GetProperty(alias); + + if (property != null && property.HasValue(culture, segment)) + return property.Value(culture, segment); + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); } - /// - /// Recursively gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. - /// - /// The target property type. - /// The content. - /// The property alias. - /// A value indicating whether to recurse. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T Value(this IPublishedContent content, string alias, bool recurse, T defaultValue) + // fixme - .Value() refactoring - in progress + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) { - return content.Value(alias, recurse, true, defaultValue); - } + var aliasesA = aliases.Split(','); + if (aliasesA.Length == 0) + return new HtmlString(string.Empty); - internal static T Value(this IPublishedContent content, string alias, bool recurse, bool withDefaultValue, T defaultValue) - { - var property = content.GetProperty(alias, recurse); - if (property == null) return defaultValue; + throw new NotImplementedException("WorkInProgress"); - return property.Value(withDefaultValue, defaultValue); + var property = content.GetProperty(aliasesA[0]); + + //var property = aliases.Split(',') + // .Where(x => string.IsNullOrWhiteSpace(x) == false) + // .Select(x => content.GetProperty(x.Trim(), recurse)) + // .FirstOrDefault(x => x != null); + + //if (format == null) format = x => x.ToString(); + + //return property != null + // ? new HtmlString(format(property.Value())) + // : new HtmlString(alt); } #endregion @@ -226,8 +223,8 @@ namespace Umbraco.Web //TODO: we should pass in the IExamineManager? var searcher = string.IsNullOrEmpty(indexName) - ? Examine.ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer) - : Examine.ExamineManager.Instance.GetSearcher(indexName); + ? ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer) + : ExamineManager.Instance.GetSearcher(indexName); if (searcher == null) throw new InvalidOperationException("No searcher found for index " + indexName); @@ -252,8 +249,8 @@ namespace Umbraco.Web //TODO: we should pass in the IExamineManager? var searcher = string.IsNullOrEmpty(indexName) - ? Examine.ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer) - : Examine.ExamineManager.Instance.GetSearcher(indexName); + ? ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer) + : ExamineManager.Instance.GetSearcher(indexName); if (searcher == null) throw new InvalidOperationException("No searcher found for index " + indexName); @@ -272,7 +269,7 @@ namespace Umbraco.Web { //TODO: we should pass in the IExamineManager? - var s = searchProvider ?? Examine.ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer); + var s = searchProvider ?? ExamineManager.Instance.GetSearcher(Constants.Examine.ExternalIndexer); var results = s.Search(criteria); return results.ToPublishedSearchResults(UmbracoContext.Current.ContentCache); @@ -307,7 +304,7 @@ namespace Umbraco.Web /// True if the content is of the specified content type; otherwise false. public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) { - return content.DocumentTypeAlias.InvariantEquals(docTypeAlias); + return content.ContentType.Alias.InvariantEquals(docTypeAlias); } /// @@ -494,7 +491,7 @@ namespace Umbraco.Web /// Does not consider the content itself. Returns all ancestors, of the specified content type. public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(false, n => n.DocumentTypeAlias == contentTypeAlias); + return content.AncestorsOrSelf(false, n => n.ContentType.Alias == contentTypeAlias); } /// @@ -559,7 +556,7 @@ namespace Umbraco.Web /// May or may not begin with the content itself, depending on its content type. public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(true, n => n.DocumentTypeAlias == contentTypeAlias); + return content.AncestorsOrSelf(true, n => n.ContentType.Alias == contentTypeAlias); } /// @@ -622,7 +619,7 @@ namespace Umbraco.Web /// Does not consider the content itself. May return null. public static IPublishedContent Ancestor(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } /// @@ -685,7 +682,7 @@ namespace Umbraco.Web /// May or may not return the content itself depending on its content type. May return null. public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } /// @@ -798,7 +795,7 @@ namespace Umbraco.Web public static IEnumerable Descendants(this IPublishedContent content, string contentTypeAlias) { - return content.DescendantsOrSelf(false, p => p.DocumentTypeAlias == contentTypeAlias); + return content.DescendantsOrSelf(false, p => p.ContentType.Alias == contentTypeAlias); } public static IEnumerable Descendants(this IPublishedContent content) @@ -825,7 +822,7 @@ namespace Umbraco.Web public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.DescendantsOrSelf(true, p => p.DocumentTypeAlias == contentTypeAlias); + return content.DescendantsOrSelf(true, p => p.ContentType.Alias == contentTypeAlias); } public static IEnumerable DescendantsOrSelf(this IPublishedContent content) @@ -852,7 +849,7 @@ namespace Umbraco.Web public static IPublishedContent Descendant(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateDescendants(false).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + return content.EnumerateDescendants(false).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } public static T Descendant(this IPublishedContent content) @@ -879,7 +876,7 @@ namespace Umbraco.Web public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateDescendants(true).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + return content.EnumerateDescendants(true).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } public static T DescendantOrSelf(this IPublishedContent content) @@ -1035,7 +1032,7 @@ namespace Umbraco.Web /// The children of the content, of any of the specified types. public static IEnumerable Children(this IPublishedContent content, params string[] alias) { - return content.Children(x => alias.InvariantContains(x.DocumentTypeAlias)); + return content.Children(x => alias.InvariantContains(x.ContentType.Alias)); } /// @@ -1111,14 +1108,14 @@ namespace Umbraco.Web ? content.Children.Any() ? content.Children.ElementAt(0) : null - : content.Children.FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAliasFilter); + : content.Children.FirstOrDefault(x => x.ContentType.Alias == contentTypeAliasFilter); if (firstNode == null) return new DataTable(); //no children found //use new utility class to create table so that we don't have to maintain code in many places, just one var dt = Core.DataTableExtensions.GenerateDataTable( //pass in the alias of the first child node since this is the node type we're rendering headers for - firstNode.DocumentTypeAlias, + firstNode.ContentType.Alias, //pass in the callback to extract the Dictionary of all defined aliases to their names alias => GetPropertyAliasesAndNames(services, alias), //pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. @@ -1131,7 +1128,7 @@ namespace Umbraco.Web { if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { - if (n.DocumentTypeAlias != contentTypeAliasFilter) + if (n.ContentType.Alias != contentTypeAliasFilter) continue; //skip this one, it doesn't match the filter } @@ -1139,7 +1136,7 @@ namespace Umbraco.Web { { "Id", n.Id }, { "NodeName", n.Name }, - { "NodeTypeAlias", n.DocumentTypeAlias }, + { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, { "CreatorName", n.CreatorName }, @@ -1187,8 +1184,8 @@ namespace Umbraco.Web /// internal static Func> GetPropertyAliasesAndNames { - get { return _getPropertyAliasesAndNames ?? GetAliasesAndNames; } - set { _getPropertyAliasesAndNames = value; } + get => _getPropertyAliasesAndNames ?? GetAliasesAndNames; + set => _getPropertyAliasesAndNames = value; } private static Dictionary GetAliasesAndNames(ServiceContext services, string alias) @@ -1225,35 +1222,5 @@ namespace Umbraco.Web } #endregion - - #region Culture - - /// - /// Return the URL name for the based on the culture specified or default culture defined - /// - /// - /// - /// - /// - public static string GetUrlName(this IPublishedContent content, ILocalizationService localizationService, string culture = null) - { - if (content.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - { - var cultureCode = culture ?? localizationService.GetDefaultLanguageIsoCode(); - if (cultureCode != null && content.CultureNames.TryGetValue(cultureCode, out var cultureName)) - { - return cultureName.UrlName; - } - //there is no name for the specified culture (unpublished perhaps) - return null; - } - else - { - //the content type is invariant - return content.UrlName; - } - } - - #endregion } } diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs index 6d397ffaa3..de9321888f 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedContentPropertyExtension.cs @@ -1,4 +1,5 @@ using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web @@ -8,25 +9,36 @@ namespace Umbraco.Web /// public static class PublishedPropertyExtension { + // see notes in PublishedElementExtensions + // + private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; + + #region Value + + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) + { + if (property.HasValue(culture, segment)) + return property.GetValue(culture, segment); + + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); + } + + #endregion + #region Value - - public static T Value(this IPublishedProperty property, string culture = null, string segment = null) - { - return property.Value(false, default(T), culture, segment); - } - - public static T Value(this IPublishedProperty property, T defaultValue, string culture = null, string segment = null) - { - return property.Value(true, defaultValue, culture, segment); - } - - internal static T Value(this IPublishedProperty property, bool withDefaultValue, T defaultValue, string culture = null, string segment = null) - { - if (property.HasValue(culture, segment) == false && withDefaultValue) return defaultValue; - - // else we use .Value so we give the converter a chance to handle the default value differently + + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default) + { + // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) + // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue + // comes right after property and conflicts with culture when T is string - so we're just not doing it - if defaultValue is + // default, whether specified or not, we give a chance to the converter + // + //if (!property.HasValue(culture, segment) && 'defaultValue is explicitely specified') return defaultValue; + + // give the converter a chance to handle the default value differently // eg for IEnumerable it may return Enumerable.Empty instead of null - + var value = property.GetValue(culture, segment); // if value is null (strange but why not) it still is OK to call TryConvertTo @@ -34,7 +46,7 @@ namespace Umbraco.Web // failed attempt. So, no need to care for value being null here. // if already the requested type, return - if (value is T) return (T)value; + if (value is T variable) return variable; // if can convert to requested type, return var convert = value.TryConvertTo(); diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index bde74c0ab1..945270cb9e 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; +using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web @@ -11,6 +12,19 @@ namespace Umbraco.Web /// public static class PublishedElementExtensions { + // lots of debates about accessing dependencies (IPublishedValueFallback) from extension methods, ranging + // from "don't do it" i.e. if an extension method is relying on dependencies, a proper service should be + // created instead, to discussing method injection vs service locator vs other subtleties, see for example + // this post http://marisks.net/2016/12/19/dependency-injection-with-extension-methods/ + // + // point is, we do NOT want a service, we DO want to write model.Value("alias", "fr-FR") and hit + // fallback somehow - which pretty much rules out method injection, and basically anything but service + // locator - bah, let's face it, it works + // + // besides, for tests, Current support setting a fallback without even a container + // + private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; + #region IsComposedOf /// @@ -54,6 +68,9 @@ namespace Umbraco.Web return prop != null && prop.HasValue(culture, segment); } + // fixme - .Value() refactoring - in progress + // missing variations... + /// /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. /// @@ -63,7 +80,7 @@ namespace Umbraco.Web /// The value to return if the content has no value for the property. /// Either or depending on whether the content /// has a value for the property identified by the alias. - public static IHtmlString IfHasValue(this IPublishedElement content, string alias, string valueIfTrue, string valueIfFalse = null) + public static IHtmlString IfValue(this IPublishedElement content, string alias, string valueIfTrue, string valueIfFalse = null) { return content.HasValue(alias) ? new HtmlString(valueIfTrue) @@ -81,27 +98,7 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. - /// The value of the content's property identified by the alias. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns null. - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null) - { - var property = content.GetProperty(alias); - return property?.GetValue(culture, segment); - } - - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. /// The default value. - /// The variation language. - /// The variation segment. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -109,31 +106,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string defaultValue, string culture = null, string segment = null) // fixme - kill + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) { var property = content.GetProperty(alias); - return property == null || property.HasValue(culture, segment) == false ? defaultValue : property.GetValue(culture, segment); - } - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. - /// The default value. - /// The variation language. - /// The variation segment. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedElement content, string alias, object defaultValue, string culture = null, string segment = null) - { - var property = content.GetProperty(alias); - return property == null || property.HasValue(culture, segment) == false ? defaultValue : property.GetValue(culture, segment); + if (property != null && property.HasValue(culture, segment)) + return property.GetValue(culture, segment); + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion @@ -148,6 +128,7 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. + /// The default value. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -155,44 +136,21 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null) - { - return content.Value(alias, false, default(T), culture, segment); - } - - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. - /// - /// The target property type. - /// The content. - /// The property alias. - /// The default value. - /// The variation language. - /// The variation segment. - /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T Value(this IPublishedElement content, string alias, T defaultValue, string culture = null, string segment = null) - { - return content.Value(alias, true, defaultValue, culture, segment); - } - - internal static T Value(this IPublishedElement content, string alias, bool withDefaultValue, T defaultValue, string culture = null, string segment = null) // fixme uh? + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) { var property = content.GetProperty(alias); - if (property == null) return defaultValue; - return property.Value(withDefaultValue, defaultValue, culture, segment); + if (property != null && property.HasValue(culture, segment)) + return property.Value(culture, segment); + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion #region Value or Umbraco.Field - WORK IN PROGRESS + // fixme - .Value() refactoring - in progress // trying to reproduce Umbraco.Field so we can get rid of it // // what we want: @@ -208,9 +166,23 @@ namespace Umbraco.Web // see UmbracoComponentRenderer.Field - which is ugly ;-( // recurse first, on each alias (that's how it's done in Field) - // TODO: strongly typed properties howto? + // // there is no strongly typed recurse, etc => needs to be in ModelsBuilder? + // that one can only happen in ModelsBuilder as that's where the attributes are defined + // the attribute that carries the alias is in ModelsBuilder! + //public static TValue Value(this TModel content, Expression> propertySelector, ...) + // where TModel : IPublishedElement + //{ + // PropertyInfo pi = GetPropertyFromExpression(propertySelector); + // var attr = pi.GetCustomAttribute(); + // var alias = attr.Alias; + // return content.Value(alias, ...) + //} + + // recurse should be implemented via fallback + + // todo - that one should be refactored, missing culture and so many things public static IHtmlString Value(this IPublishedElement content, string aliases, Func format, string alt = "") { if (format == null) format = x => x.ToString(); @@ -225,21 +197,6 @@ namespace Umbraco.Web : new HtmlString(alt); } - // fixme - move that one! - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) - { - if (format == null) format = x => x.ToString(); - - var property = aliases.Split(',') - .Where(x => string.IsNullOrWhiteSpace(x) == false) - .Select(x => content.GetProperty(x.Trim(), recurse)) - .FirstOrDefault(x => x != null); - - return property != null - ? new HtmlString(format(property.Value())) - : new HtmlString(alt); - } - #endregion #region ToIndexedArray diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs index 4b7cb48add..7b7a70cc2a 100644 --- a/src/Umbraco.Web/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; -using Umbraco.Web.PublishedCache; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Routing { @@ -17,15 +14,13 @@ namespace Umbraco.Web.Routing public class AliasUrlProvider : IUrlProvider { private readonly IGlobalSettings _globalSettings; - private readonly IRequestHandlerSection _requestConfig; - private readonly ILocalizationService _localizationService; + private readonly IRequestHandlerSection _requestConfig; private readonly ISiteDomainHelper _siteDomainHelper; - public AliasUrlProvider(IGlobalSettings globalSettings, IRequestHandlerSection requestConfig, ILocalizationService localizationService, ISiteDomainHelper siteDomainHelper) + public AliasUrlProvider(IGlobalSettings globalSettings, IRequestHandlerSection requestConfig, ISiteDomainHelper siteDomainHelper) { _globalSettings = globalSettings; - _requestConfig = requestConfig; - _localizationService = localizationService; + _requestConfig = requestConfig; _siteDomainHelper = siteDomainHelper; } @@ -35,20 +30,8 @@ namespace Umbraco.Web.Routing #region GetUrl - /// - /// Gets the nice url of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute url. - /// The url mode. - /// The url for the published content. - /// - /// The url is absolute or relative depending on url indicated by current and settings, unless - /// absolute is true, in which case the url is always absolute. - /// If the provider is unable to provide a url, it should return null. - /// - public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture = null) + /// + public string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) { return null; // we have nothing to say } diff --git a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs index e75c733290..ce1fc1ffab 100644 --- a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs +++ b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Mvc; namespace Umbraco.Web.Routing @@ -15,29 +15,19 @@ namespace Umbraco.Web.Routing /// /// This will return the URL that is returned by the assigned custom if this is a custom route /// - /// - /// - /// - /// - /// - public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture = null) + public string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) { - if (umbracoContext == null) return null; - if (umbracoContext.PublishedRequest == null) return null; - if (umbracoContext.PublishedRequest.PublishedContent == null) return null; - if (umbracoContext.HttpContext == null) return null; - if (umbracoContext.HttpContext.Request == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext.RouteData == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.CustomRouteDataToken) == false) return null; + if (umbracoContext?.PublishedRequest?.PublishedContent == null) return null; + if (umbracoContext.HttpContext?.Request?.RequestContext?.RouteData?.DataTokens == null) return null; + if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey(Core.Constants.Web.CustomRouteDataToken) == false) return null; + //If we get this far, it means it's a custom route with published content assigned, check if the id being requested for is the same id as the assigned published content //NOTE: This looks like it might cause an infinite loop because PublishedContentBase.Url calls into UmbracoContext.Current.UrlProvider.GetUrl which calls back into the IUrlProvider pipeline // but the specific purpose of this is that a developer is using their own IPublishedContent that returns a specific Url and doesn't go back into the UrlProvider pipeline. //TODO: We could put a try/catch here just in case, else we could do some reflection checking to see if the implementation is PublishedContentBase and the Url property is not overridden. - return id == umbracoContext.PublishedRequest.PublishedContent.Id - ? umbracoContext.PublishedRequest.PublishedContent.Url + return content.Id == umbracoContext.PublishedRequest.PublishedContent.Id + ? umbracoContext.PublishedRequest.PublishedContent.GetUrl(culture) : null; } diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 37d38d521e..366bcd865f 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; - +using Umbraco.Core.Models.PublishedContent; + namespace Umbraco.Web.Routing { /// @@ -31,27 +28,15 @@ namespace Umbraco.Web.Routing #region GetUrl - /// - /// Gets the url of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute url. - /// The url mode. - /// The culture. - /// The url for the published content. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the provider is unable to provide a url, it should return null. - /// - public virtual string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture = null) + /// + public virtual string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) { if (!current.IsAbsoluteUri) throw new ArgumentException("Current url must be absolute.", nameof(current)); - + // will not use cache if previewing - var route = umbracoContext.ContentCache.GetRouteById(id, culture); + var route = umbracoContext.ContentCache.GetRouteById(content.Id, culture); - return GetUrlFromRoute(route, umbracoContext, id, current, mode, culture); + return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); } internal string GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture) diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 16a3fc92ad..e3e214a144 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -156,7 +156,7 @@ namespace Umbraco.Web.Routing { if (cultureDomains.Count == 1) // only 1, return return cultureDomains.First(); - + // else restrict to those domains, for base lookup considerForBaseDomains = cultureDomains; } @@ -165,7 +165,7 @@ namespace Umbraco.Web.Routing var baseDomains = SelectByBase(considerForBaseDomains, uri); if (baseDomains.Count > 0) // found, return return baseDomains.First(); - + // if nothing works, then try to run the filter to select a domain // either restricting on cultureDomains, or on all domains if (filter != null) @@ -181,17 +181,21 @@ namespace Umbraco.Web.Routing return null; } + private static bool IsBaseOf(DomainAndUri domain, Uri uri) + => domain.Uri.EndPathWithSlash().IsBaseOf(uri); + private static IReadOnlyCollection SelectByBase(IReadOnlyCollection domainsAndUris, Uri uri) { // look for domains that would be the base of the uri // ie current is www.example.com/foo/bar, look for domain www.example.com var currentWithSlash = uri.EndPathWithSlash(); - var baseDomains = domainsAndUris.Where(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash)).ToList(); + var baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithSlash)).ToList(); // if none matches, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com + var currentWithoutPort = currentWithSlash.WithoutPort(); if (baseDomains.Count == 0) - baseDomains = domainsAndUris.Where(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash.WithoutPort())).ToList(); + baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithoutPort)).ToList(); return baseDomains; } diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index 031f4670b2..53a7e9886c 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; -using Umbraco.Web.PublishedCache; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Routing { @@ -14,16 +13,18 @@ namespace Umbraco.Web.Routing /// Gets the nice url of a published content. /// /// The Umbraco context. - /// The published content id. - /// The current absolute url. + /// The published content. /// The url mode. + /// A culture. + /// The current absolute url. /// The url for the published content. /// /// The url is absolute or relative depending on mode and on current. + /// If the published content is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it should return null. /// - string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture = null); - // FIXME WE HAVE TO DOCUMENT CULTURE FFS + string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current); /// /// Gets the other urls of a published content. diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index add9c4ad3b..78290401c0 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -285,7 +285,7 @@ namespace Umbraco.Web.Routing if (!contentForDomain.ContentType.Variations.Has(ContentVariation.CultureNeutral)) return true; //variant so ensure the culture name exists - return contentForDomain.CultureNames.ContainsKey(x.Culture.Name); + return contentForDomain.Cultures.ContainsKey(x.Culture.Name); }).ToList(); var defaultCulture = domainsCache.DefaultCulture; @@ -554,7 +554,7 @@ namespace Umbraco.Web.Routing var redirect = false; var valid = false; IPublishedContent internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId, -1); + var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); if (internalRedirectId > 0) { @@ -760,7 +760,7 @@ namespace Umbraco.Web.Routing if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) return; - var redirectId = request.PublishedContent.Value(Constants.Conventions.Content.Redirect, -1); + var redirectId = request.PublishedContent.Value(Constants.Conventions.Content.Redirect, defaultValue: -1); var redirectUrl = "#"; if (redirectId > 0) { diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index c23ff5fb86..a1fd31a127 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Web.PublishedCache; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Web.Composing; -using Umbraco.Core.Services; -using System.Globalization; - +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + namespace Umbraco.Web.Routing { /// @@ -23,15 +20,16 @@ namespace Umbraco.Web.Routing /// Initializes a new instance of the class with an Umbraco context and a list of url providers. /// /// The Umbraco context. - /// + /// Routing settings. /// The list of url providers. - public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IEntityService entityService) + /// The current variation accessor. + public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor) { if (routingSettings == null) throw new ArgumentNullException(nameof(routingSettings)); _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _urlProviders = urlProviders; - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _urlProviders = urlProviders; + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); var provider = UrlProviderMode.Auto; Mode = provider; @@ -46,18 +44,20 @@ namespace Umbraco.Web.Routing /// /// The Umbraco context. /// The list of url providers. - /// - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, UrlProviderMode provider = UrlProviderMode.Auto) + /// The current variation accessor. + /// An optional provider mode. + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _variationContextAccessor = variationContextAccessor; - Mode = provider; + Mode = mode; } private readonly UmbracoContext _umbracoContext; private readonly IEnumerable _urlProviders; - private readonly IEntityService _entityService; + private readonly IVariationContextAccessor _variationContextAccessor; /// /// Gets or sets the provider url mode. @@ -68,150 +68,149 @@ namespace Umbraco.Web.Routing #region GetUrl + private UrlProviderMode GetMode(bool absolute) => absolute ? UrlProviderMode.Absolute : Mode; + private IPublishedContent GetDocument(int id) => _umbracoContext.ContentCache.GetById(id); + private IPublishedContent GetDocument(Guid id) => _umbracoContext.ContentCache.GetById(id); + /// /// Gets the url of a published content. /// - /// The published content identifier. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on the current url. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(Guid id, string culture = null) - { - var intId = _entityService.GetId(id, UmbracoObjectTypes.Document); - return GetUrl(intId.Success ? intId.Result : -1, culture); - } - - /// - /// Gets the nice url of a published content. - /// - /// The published content identifier. - /// A value indicating whether the url should be absolute in any case. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(Guid id, bool absolute, string culture = null) - { - var intId = _entityService.GetId(id, UmbracoObjectTypes.Document); - return GetUrl(intId.Success ? intId.Result : -1, absolute, culture); - } - - /// - /// Gets the nice url of a published content. - /// - /// The published content id. + /// The published content. + /// A culture. /// The current absolute url. - /// A value indicating whether the url should be absolute in any case. /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(Guid id, Uri current, bool absolute, string culture = null) - { - var intId = _entityService.GetId(id, UmbracoObjectTypes.Document); - return GetUrl(intId.Success ? intId.Result : -1, current, absolute, culture); - } + public string GetUrl(IPublishedContent content, string culture = null, Uri current = null) + => GetUrl(content, Mode, culture, current); /// /// Gets the nice url of a published content. /// - /// The published content identifier. - /// The url mode. + /// The published content. + /// A value indicating whether the url should be absolute in any case. + /// A culture. + /// The current absolute url. /// The url for the published content. /// - /// The url is absolute or relative depending on mode and on the current url. - /// If the provider is unable to provide a url, it returns "#". + /// The url is absolute or relative depending on Mode and on current, unless + /// absolute is true, in which case the url is always absolute. /// - public string GetUrl(Guid id, UrlProviderMode mode, string culture = null) - { - var intId = _entityService.GetId(id, UmbracoObjectTypes.Document); - return GetUrl(intId.Success ? intId.Result : -1, mode, culture); - } + public string GetUrl(IPublishedContent content, bool absolute, Uri current = null, string culture = null) + => GetUrl(content, GetMode(absolute), culture, current); + + /// + /// Gets the nice url of a published content. + /// + /// The published content. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + public string GetUrl(IPublishedContent content, UrlProviderMode mode, Uri current = null, string culture = null) + => GetUrl(content, mode, culture, current); /// /// Gets the url of a published content. /// /// The published content identifier. + /// A culture. + /// The current absolute url. /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on the current url. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(int id, string culture = null) - { - return GetUrl(id, _umbracoContext.CleanedUmbracoUrl, Mode, culture); - } + public string GetUrl(Guid id, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), Mode, culture, current); /// /// Gets the nice url of a published content. /// /// The published content identifier. /// A value indicating whether the url should be absolute in any case. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(int id, bool absolute, string culture = null) - { - var mode = absolute ? UrlProviderMode.Absolute : Mode; - return GetUrl(id, _umbracoContext.CleanedUmbracoUrl, mode, culture); - } - - /// - /// Gets the nice url of a published content. - /// - /// The published content id. + /// A culture. /// The current absolute url. - /// A value indicating whether the url should be absolute in any case. /// The url for the published content. /// /// The url is absolute or relative depending on Mode and on current, unless /// absolute is true, in which case the url is always absolute. - /// If the provider is unable to provide a url, it returns "#". /// - public string GetUrl(int id, Uri current, bool absolute, string culture = null) - { - var mode = absolute ? UrlProviderMode.Absolute : Mode; - return GetUrl(id, current, mode, culture); - } + public string GetUrl(Guid id, bool absolute, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), GetMode(absolute), culture, current); /// /// Gets the nice url of a published content. /// /// The published content identifier. /// The url mode. + /// A culture. + /// The current absolute url. /// The url for the published content. - /// - /// The url is absolute or relative depending on mode and on the current url. - /// If the provider is unable to provide a url, it returns "#". - /// - public string GetUrl(int id, UrlProviderMode mode, string culture = null) - { - return GetUrl(id, _umbracoContext.CleanedUmbracoUrl, mode, culture); - } + public string GetUrl(Guid id, UrlProviderMode mode, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), mode, culture, current); + + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// A culture. + /// The current absolute url. + /// The url for the published content. + public string GetUrl(int id, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), Mode, culture, current); /// /// Gets the nice url of a published content. /// - /// The published content id. + /// The published content identifier. + /// A value indicating whether the url should be absolute in any case. + /// A culture. /// The current absolute url. + /// The url for the published content. + /// + /// The url is absolute or relative depending on Mode and on current, unless + /// absolute is true, in which case the url is always absolute. + /// + public string GetUrl(int id, bool absolute, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), GetMode(absolute), culture, current); + + /// + /// Gets the nice url of a published content. + /// + /// The published content identifier. /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + public string GetUrl(int id, UrlProviderMode mode, string culture = null, Uri current = null) + => GetUrl(GetDocument(id), mode, culture, current); + + /// + /// Gets the nice url of a published content. + /// + /// The published content. + /// The url mode. + /// A culture. + /// The current absolute url. /// The url for the published content. /// /// The url is absolute or relative depending on mode and on current. + /// If the published content is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns "#". /// - public string GetUrl(int id, Uri current, UrlProviderMode mode, string culture = null) // FIXME DOCUMENT + public string GetUrl(IPublishedContent content, UrlProviderMode mode, string culture = null, Uri current = null) { - var url = _urlProviders.Select(provider => provider.GetUrl(_umbracoContext, id, current, mode, culture)) + if (content == null) + return "#"; + + // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture + if (culture == null) + { + culture = content.ContentType.Variations.Has(ContentVariation.CultureNeutral) // fixme CultureSegment + ? _variationContextAccessor.VariationContext.Culture + : null; + } + + if (current == null) + current = _umbracoContext.CleanedUmbracoUrl; + + var url = _urlProviders.Select(provider => provider.GetUrl(_umbracoContext, content, mode, culture, current)) .FirstOrDefault(u => u != null); return url ?? "#"; // legacy wants this } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 196fd10650..460f881dae 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -24,6 +24,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Macros; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; @@ -38,6 +39,7 @@ using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Install; using Umbraco.Web.Media; +using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -68,8 +70,9 @@ namespace Umbraco.Web.Runtime //it still needs to use the install controller so we can't do that composition.Container.RegisterFrom(); - // register the system culture provider - composition.Container.RegisterSingleton(); + // register accessors for cultures + composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); var typeLoader = composition.Container.GetInstance(); var logger = composition.Container.GetInstance(); @@ -194,6 +197,9 @@ namespace Umbraco.Web.Runtime // register preview SignalR hub composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); + + // register properties fallback + composition.Container.RegisterSingleton(); } internal void Initialize( @@ -206,6 +212,7 @@ namespace Umbraco.Web.Runtime IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, IEntityService entityService, + IVariationContextAccessor variationContextAccessor, UrlProviderCollection urlProviders) { // setup mvc and webapi services @@ -243,7 +250,7 @@ namespace Umbraco.Web.Runtime umbracoSettings, urlProviders, globalSettings, - entityService); + variationContextAccessor); // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); @@ -395,7 +402,7 @@ namespace Umbraco.Web.Runtime // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not // utilizing an old path appDomainHash); - + //set the file map and composite file default location to the %temp% location BaseCompositeFileProcessingProvider.CompositeFilePathDefaultFolder = XmlFileMapper.FileMapDefaultFolder diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index e56eae81e8..8967fd5165 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -209,33 +209,48 @@ namespace Umbraco.Web.Trees { result = Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToArray(); } - - //This should really never be null, but we'll error check anyways - culture = culture ?? Services.LocalizationService.GetDefaultLanguageIsoCode(); - - //Try to see if there is a variant name for the current language for the item and set the name accordingly. - //If any of this fails, the tree node name will remain the default invariant culture name. - - //fixme - what if there is no name found at all ? This could occur if the doc type is variant and the user fills in all language values, then creates a new lang and sets it as the default - //fixme - what if the user changes this document type to not allow culture variants after it's already been created with culture variants, this means we'll be displaying the culture variant name when in fact we should be displaying the invariant name... but that would be null - + + // should really never be null, but we'll error check anyways + culture = culture ?? Services.LocalizationService.GetDefaultLanguageIsoCode(); + + // set names according to variations if (!culture.IsNullOrWhiteSpace()) - { - foreach (var e in result) - { - if (e is IDocumentEntitySlim doc) - { - if (doc.CultureNames.TryGetValue(culture, out var name)) - { - e.Name = name; - } - } - } - } + foreach (var entity in result) + EnsureName(entity, culture); return result; } + // set name according to variations + // + private void EnsureName(IEntitySlim entity, string culture) + { + if (culture == null) + { + if (string.IsNullOrWhiteSpace(entity.Name)) + entity.Name = "[[" + entity.Id + "]]"; + return; + } + + // we are getting the tree for a given culture, + // for those items that DO support cultures, we need to get the proper name, IF it exists + // otherwise, invariant is fine + + if (entity.AdditionalData.TryGetValue(EntitySlim.AdditionalVariations, out var variationsObject) && + variationsObject is ContentVariation variations && + variations.Has(ContentVariation.CultureNeutral) && + entity.AdditionalData.TryGetValue(EntitySlim.AdditionalCultureNames, out var namesObject) && + namesObject is IDictionary names && + names.TryGetValue(culture, out var name) && + !string.IsNullOrWhiteSpace(name)) + { + entity.Name = name; + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + entity.Name = "[[" + entity.Id + "]]"; + } + /// /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 86d20ca440..efdea63d96 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -260,6 +260,7 @@ + @@ -336,13 +337,14 @@ - + + @@ -364,7 +366,7 @@ - + @@ -381,8 +383,9 @@ - + + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index b86c8c29a6..a88716bec3 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -4,6 +4,7 @@ using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -67,9 +68,9 @@ namespace Umbraco.Web IPublishedSnapshotService publishedSnapshotService, WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, - IEnumerable urlProviders, - IGlobalSettings globalSettings, - IEntityService entityService, + IEnumerable urlProviders, + IGlobalSettings globalSettings, + IVariationContextAccessor variationContextAccessor, bool replace = false) { if (umbracoContextAccessor == null) throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -87,7 +88,7 @@ namespace Umbraco.Web // create & assign to accessor, dispose existing if any umbracoContextAccessor.UmbracoContext?.Dispose(); - return umbracoContextAccessor.UmbracoContext = new UmbracoContext(httpContext, publishedSnapshotService, webSecurity, umbracoSettings, urlProviders, globalSettings, entityService); + return umbracoContextAccessor.UmbracoContext = new UmbracoContext(httpContext, publishedSnapshotService, webSecurity, umbracoSettings, urlProviders, globalSettings, variationContextAccessor); } // initializes a new instance of the UmbracoContext class @@ -100,13 +101,14 @@ namespace Umbraco.Web IUmbracoSettingsSection umbracoSettings, IEnumerable urlProviders, IGlobalSettings globalSettings, - IEntityService entityService) + IVariationContextAccessor variationContextAccessor) { if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); + VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); // ensure that this instance is disposed when the request terminates, though we *also* ensure @@ -136,7 +138,7 @@ namespace Umbraco.Web // OriginalRequestUrl = GetRequestFromContext()?.Url ?? new Uri("http://localhost"); CleanedUmbracoUrl = UriUtility.UriToUmbraco(OriginalRequestUrl); - UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, entityService); + UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, variationContextAccessor); } #endregion @@ -213,6 +215,8 @@ namespace Umbraco.Web /// public HttpContextBase HttpContext { get; } + public IVariationContextAccessor VariationContextAccessor { get; } + /// /// Creates and caches an instance of a DomainHelper /// diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 24189fd601..8c78ead696 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -1,10 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Text; -using System.Threading; using System.Web; using System.Web.Routing; using LightInject; @@ -17,12 +15,12 @@ using Umbraco.Web.Security; using Umbraco.Core.Collections; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; -using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; namespace Umbraco.Web { @@ -55,9 +53,6 @@ namespace Umbraco.Web [Inject] public IUserService UserService { get; set; } - [Inject] - public IEntityService EntityService { get; set; } - [Inject] public UrlProviderCollection UrlProviders { get; set; } @@ -71,7 +66,10 @@ namespace Umbraco.Web internal PublishedRouter PublishedRouter { get; set; } [Inject] - internal IUmbracoDatabaseFactory DatabaseFactory { get; set; } + internal IUmbracoDatabaseFactory DatabaseFactory { get; set; } + + [Inject] + internal IVariationContextAccessor VariationContextAccessor { get; set; } #endregion @@ -114,8 +112,8 @@ namespace Umbraco.Web new WebSecurity(httpContext, UserService, GlobalSettings), UmbracoConfig.For.UmbracoSettings(), UrlProviders, - GlobalSettings, - EntityService, + GlobalSettings, + VariationContextAccessor, true); } diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 9c01e02136..bd36ee429c 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -75,7 +75,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, recursive); + var pval = publishedContent.Value(_fieldName, recurse: recursive); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -95,7 +95,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, recursive); + var pval = publishedContent.Value(altFieldName, recurse: recursive); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index 8fe05370ce..c716e8ea46 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -65,7 +65,7 @@ namespace umbraco throw new ArgumentException("Document request has no node.", "frequest"); populatePageData(frequest.PublishedContent.Id, - frequest.PublishedContent.Name, frequest.PublishedContent.DocumentTypeId, frequest.PublishedContent.DocumentTypeAlias, + frequest.PublishedContent.Name, frequest.PublishedContent.ContentType.Id, frequest.PublishedContent.ContentType.Alias, frequest.PublishedContent.WriterName, frequest.PublishedContent.CreatorName, frequest.PublishedContent.CreateDate, frequest.PublishedContent.UpdateDate, frequest.PublishedContent.Path, frequest.PublishedContent.Parent == null ? -1 : frequest.PublishedContent.Parent.Id); @@ -89,7 +89,7 @@ namespace umbraco if (doc == null) throw new ArgumentNullException("doc"); populatePageData(doc.Id, - doc.Name, doc.DocumentTypeId, doc.DocumentTypeAlias, + doc.Name, doc.ContentType.Id, doc.ContentType.Alias, doc.WriterName, doc.CreatorName, doc.CreateDate, doc.UpdateDate, doc.Path, doc.Parent == null ? -1 : doc.Parent.Id); @@ -108,8 +108,8 @@ namespace umbraco /// /// The content. /// This is for usage only. - internal page(IContent content) - : this(new PagePublishedContent(content)) + internal page(IContent content, IVariationContextAccessor variationContextAccessor) + : this(new PagePublishedContent(content, variationContextAccessor)) { } #endregion @@ -393,19 +393,23 @@ namespace umbraco private readonly PublishedContentType _contentType; private readonly IPublishedProperty[] _properties; private readonly IPublishedContent _parent; - private IReadOnlyDictionary _cultureNames; + private IReadOnlyDictionary _cultureInfos; + private readonly IVariationContextAccessor _variationContextAccessor; + + private static readonly IReadOnlyDictionary NoCultureInfos = new Dictionary(); private PagePublishedContent(int id) { _id = id; } - public PagePublishedContent(IContent inner) + public PagePublishedContent(IContent inner, IVariationContextAccessor variationContextAccessor) { if (inner == null) throw new NullReferenceException("content"); _inner = inner; + _variationContextAccessor = variationContextAccessor; _id = _inner.Id; _key = _inner.Key; @@ -457,27 +461,35 @@ namespace umbraco get { return _inner.Name; } } - public IReadOnlyDictionary CultureNames + public PublishedCultureInfos GetCulture(string culture = null) { - get - { - if (!_inner.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - return null; - - if (_cultureNames == null) - { - var d = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (var c in _inner.Names) - { - d[c.Key] = new PublishedCultureName(c.Value, c.Value.ToUrlSegment()); - } - _cultureNames = d; - } - return _cultureNames; + // handle context culture + if (culture == null) + culture = _variationContextAccessor.VariationContext.Culture; + + // no invariant culture infos + if (culture == "") return null; + + // get + return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; + } + + public IReadOnlyDictionary Cultures + { + get + { + if (!_inner.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) // fixme CultureSegment? + return NoCultureInfos; + + if (_cultureInfos != null) + return _cultureInfos; + + return _cultureInfos = _inner.PublishNames + .ToDictionary(x => x.Key, x => new PublishedCultureInfos(x.Key, x.Value, _inner.GetCulturePublishDate(x.Key))); } } - public string UrlName + public string UrlSegment { get { throw new NotImplementedException(); } } @@ -537,6 +549,8 @@ namespace umbraco get { throw new NotImplementedException(); } } + public string GetUrl(string culture = null) => throw new NotSupportedException(); + public PublishedItemType ItemType { get { return PublishedItemType.Content; }