diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index 14cb0dce8e..0510740e42 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing } } - private IEnumerable PrepareComposerTypes() + internal IEnumerable PrepareComposerTypes() + { + var requirements = GetRequirements(); + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + + var sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary> GetRequirements(bool throwOnMissing = true) { // create a list, remove those that cannot be enabled due to runtime level var composerTypeList = _composerTypes @@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing // enable or disable composers EnableDisableComposers(composerTypeList); - // sort the composers according to their dependencies - var requirements = new Dictionary>(); - foreach (var type in composerTypeList) requirements[type] = null; - foreach (var type in composerTypeList) + void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); - GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + foreach (var attribute in type.GetCustomAttributes()) + { + var typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list + { + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); + + // add all its interfaces implementing IComposer + foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) + { + iset.Add(i); + set2.Add(i); + } + } + } } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + var list1 = composerTypeList; + while (list1.Count > 0) + { + var list2 = new List(); + foreach (var t in list1) + { + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } + composerTypeList.AddRange(list2); + list1 = list2; + } + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) + requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary> requirements) + { // sort composers var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); graph.AddItems(requirements); List sortedComposerTypes; try { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); } catch (Exception e) { @@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing throw; } - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - return sortedComposerTypes; } - private static string GetComposersReport(Dictionary> requirements) + internal static string GetComposersReport(Dictionary> requirements) { var text = new StringBuilder(); text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); text.AppendLine(); + bool HasReq(IEnumerable types, Type type) + => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); + foreach (var kvp in requirements) { var type = kvp.Key; text.AppendLine(type.FullName); foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) { - text.AppendLine(" : " + i.FullName); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" < " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + text.AppendLine(" : " + i.FullName); if (kvp.Value != null) foreach (var t in kvp.Value) text.AppendLine(" = " + t); @@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing types.Remove(kvp.Key); } - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary> requirements, bool throwOnMissing = true) { // get 'require' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type + var afterAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in requireAttributes) + foreach (var attr in afterAttributes) { if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) @@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing // unless strong, and then require at least one enabled composer implementing that interface if (attr.RequiredType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); if (implems.Count > 0) { if (requirements[type] == null) requirements[type] = new List(); requirements[type].AddRange(implems); } - else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } // requiring a class = require that the composer is enabled @@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing if (requirements[type] == null) requirements[type] = new List(); requirements[type].Add(attr.RequiredType); } - else if (attr.Weak != true) // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } } - private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary> requirements) { // get 'required' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requiredAttributes = type + var beforeAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer - foreach (var attr in requiredAttributes) + foreach (var attr in beforeAttributes) { if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) // required by an interface = by any enabled composer implementing this that interface if (attr.RequiringType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); foreach (var implem in implems) { if (requirements[implem] == null) requirements[implem] = new List(); diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index d1c2d23c4f..0a9d750964 100644 --- a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,8 +1,8 @@ namespace Umbraco.Core.PropertyEditors { - internal class DropDownFlexibleConfiguration : ValueListConfiguration + public class DropDownFlexibleConfiguration : ValueListConfiguration { [ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] public bool Multiple { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index c026e5a157..2ba94d8c78 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; composers = new Composers(composition, types, Mock.Of()); @@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Composer27), Composed[1]); } + [Test] + public void AllComposers() + { + var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of()); + + var register = MockRegister(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + + var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); + var composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(); + var report = Composers.GetComposersReport(requirements); + Console.WriteLine(report); + var composerTypes = composers.SortComposers(requirements); + + foreach (var type in composerTypes) + Console.WriteLine(type); + } + #region Compothings public class TestComposerBase : IComposer diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 5fc0d628c9..2a144f3aaa 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -381,6 +381,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture)); diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index 71490465d0..0c7ee98c6d 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -97,6 +97,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetUmbracoMedia(contentId) != null; diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 86017be820..9828a14597 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -92,6 +92,9 @@ namespace Umbraco.Tests.PublishedContent throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return _content.ContainsKey(contentId); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index b66ab40335..8358dc4b67 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -228,40 +228,64 @@ body.touch .umb-tree { } } -.umb-tree-item__annotation { - &::before { - font-family: 'icomoon'; - position: absolute; - bottom: 0; - } -} +.has-unpublished-version, .is-container, .protected { + > .umb-tree-item__inner { + > .umb-tree-item__annotation { + background-color: @white; + border-radius: 50%; + width: 12px; + height: 12px; + position: absolute; + margin-left: 12px; + top: 17px; -.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { - content: "\e25a"; - color: @green; - font-size: 20px; - margin-left: -25px; + &::before { + font-family: 'icomoon'; + position: absolute; + top: -4px; + } + } + + &:hover > .umb-tree-item__annotation { + background-color: @ui-option-hover; + } + } + + &.current > .umb-tree-item__inner > .umb-tree-item__annotation { + background-color: @pinkLight; + } } .is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; +} + +.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { + content: "\e25a"; + color: @green; + font-size: 23px; + margin-left: 16px; + left: -21px; } .protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e256"; color: @red; - font-size: 20px; - margin-left: -25px; + font-size: 23px; + margin-left: -3px; + left: -2px; } .locked > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e0a7"; color: @red; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; } .no-access { diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index ff459a2d9b..3cd7b924fb 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -29,6 +30,15 @@ namespace Umbraco.Web.PublishedCache /// The value of overrides defaults. IPublishedContent GetById(bool preview, Guid contentId); + /// + /// Gets a content identified by its Udi identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content Udi identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent GetById(bool preview, Udi contentId); + /// /// Gets a content identified by its unique identifier. /// @@ -45,6 +55,14 @@ namespace Umbraco.Web.PublishedCache /// Considers published or unpublished content depending on defaults. IPublishedContent GetById(Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent GetById(Udi contentId); + /// /// Gets a value indicating whether the cache contains a specified content. /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 0e74ea919f..4bd3fcf247 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -238,6 +238,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return GetNodePublishedContent(node, preview); } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Document) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); + + return GetById(preview, guidUdi.Guid); + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index f7bdb4400f..112ccd9931 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -44,6 +45,20 @@ namespace Umbraco.Web.PublishedCache.NuCache return n?.PublishedModel; } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Media) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Media}\".", nameof(contentId)); + + // ignore preview, there's only draft for media + var n = _snapshot.Get(guidUdi.Guid); + return n?.PublishedModel; + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index b0fe1a4240..b88ae26704 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -19,23 +20,22 @@ namespace Umbraco.Web.PublishedCache public abstract IPublishedContent GetById(bool preview, int contentId); public IPublishedContent GetById(int contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); public abstract IPublishedContent GetById(bool preview, Guid contentId); public IPublishedContent GetById(Guid contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); + + public abstract IPublishedContent GetById(bool preview, Udi contentId); + + public IPublishedContent GetById(Udi contentId) + => GetById(PreviewDefault, contentId); public abstract bool HasById(bool preview, int contentId); public bool HasById(int contentId) - { - return HasById(PreviewDefault, contentId); - } + => HasById(PreviewDefault, contentId); public abstract IEnumerable GetAtRoot(bool preview); diff --git a/src/Umbraco.Web/Runtime/WebFinalComposer.cs b/src/Umbraco.Web/Runtime/WebFinalComposer.cs index 64d7725848..c69ae1af1a 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComposer.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComposer.cs @@ -3,7 +3,9 @@ namespace Umbraco.Web.Runtime { // web's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] public class WebFinalComposer : ComponentComposer { } }