diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0c1a202b66..92f8529bbe 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -28,10 +28,7 @@ namespace Umbraco.Core.Cache [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string MacroCacheKey = "UmbracoMacroCache"; - public const string MacroHtmlCacheKey = "macroHtml_"; - public const string MacroControlCacheKey = "macroControl_"; - public const string MacroHtmlDateAddedCacheKey = "macroHtml_DateAdded_"; - public const string MacroControlDateAddedCacheKey = "macroControl_DateAdded_"; + public const string MacroContentCacheKey = "macroContent_"; // for macro contents [UmbracoWillObsolete("This cache key is only used for legacy 'library' member caching, remove in v8")] public const string MemberLibraryCacheKey = "UL_GetMember"; diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index e9b63976d2..00f1320525 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -6,79 +6,115 @@ using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// - /// A base class for cache refreshers to inherit from that ensures the correct events are raised - /// when cache refreshing occurs. + /// A base class for cache refreshers that handles events. /// - /// The real cache refresher type, this is used for raising strongly typed events + /// The actual cache refresher type. + /// The actual cache refresher type is used for strongly typed events. public abstract class CacheRefresherBase : ICacheRefresher - where TInstanceType : ICacheRefresher + where TInstanceType : class, ICacheRefresher { + /// + /// Initializes a new instance of the . + /// + /// A cache helper. protected CacheRefresherBase(CacheHelper cacheHelper) { CacheHelper = cacheHelper; } /// - /// An event that is raised when cache is updated on an individual server + /// Triggers when the cache is updated on the server. /// /// - /// This event will fire on each server configured for an Umbraco project whenever a cache refresher - /// is updated. + /// Triggers on each server configured for an Umbraco project whenever a cache refresher is updated. /// public static event TypedEventHandler CacheUpdated; - /// - /// Raises the event - /// - /// - /// - protected static void OnCacheUpdated(TInstanceType sender, CacheRefresherEventArgs args) - { - if (CacheUpdated != null) - { - CacheUpdated(sender, args); - } - } + #region Define /// - /// Returns the real instance of the object ('this') for use in strongly typed events + /// Gets the typed 'this' for events. /// protected abstract TInstanceType Instance { get; } - public abstract Guid UniqueIdentifier { get; } + /// + /// Gets the unique identifier of the refresher. + /// + public abstract Guid RefresherUniqueId { get; } + /// + /// Gets the name of the refresher. + /// public abstract string Name { get; } - protected CacheHelper CacheHelper { get; } + #endregion + #region Refresher + + /// + /// Refreshes all entities. + /// public virtual void RefreshAll() { OnCacheUpdated(Instance, new CacheRefresherEventArgs(null, MessageType.RefreshAll)); } + /// + /// Refreshes an entity. + /// + /// The entity's identifier. public virtual void Refresh(int id) { OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RefreshById)); } - public virtual void Remove(int id) - { - OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RemoveById)); - } - + /// + /// Refreshes an entity. + /// + /// The entity's identifier. public virtual void Refresh(Guid id) { OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RefreshById)); } /// - /// Clears the cache for all repository entities of this type + /// Removes an entity. /// - /// - internal void ClearAllIsolatedCacheByEntityType() + /// The entity's identifier. + public virtual void Remove(int id) + { + OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RemoveById)); + } + + #endregion + + #region Protected + + /// + /// Gets the cache helper. + /// + protected CacheHelper CacheHelper { get; } + + /// + /// Clears the cache for all repository entities of a specified type. + /// + /// The type of the entities. + protected void ClearAllIsolatedCacheByEntityType() where TEntity : class, IAggregateRoot { CacheHelper.IsolatedRuntimeCache.ClearCache(); } + + /// + /// Raises the CacheUpdated event. + /// + /// The event sender. + /// The event arguments. + protected static void OnCacheUpdated(TInstanceType sender, CacheRefresherEventArgs args) + { + CacheUpdated?.Invoke(sender, args); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/CacheRefreshersResolver.cs b/src/Umbraco.Core/Cache/CacheRefreshersResolver.cs index 4f91739068..1c2cff208f 100644 --- a/src/Umbraco.Core/Cache/CacheRefreshersResolver.cs +++ b/src/Umbraco.Core/Cache/CacheRefreshersResolver.cs @@ -38,7 +38,7 @@ namespace Umbraco.Core.Cache /// The value of the type uniquely identified by . public ICacheRefresher GetById(Guid id) { - return Values.FirstOrDefault(x => x.UniqueIdentifier == id); + return Values.FirstOrDefault(x => x.RefresherUniqueId == id); } } diff --git a/src/Umbraco.Core/Cache/ICacheRefresher.cs b/src/Umbraco.Core/Cache/ICacheRefresher.cs index 0117681ecd..291b59dadd 100644 --- a/src/Umbraco.Core/Cache/ICacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ICacheRefresher.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Cache /// public interface ICacheRefresher { - Guid UniqueIdentifier { get; } + Guid RefresherUniqueId { get; } string Name { get; } void RefreshAll(); void Refresh(int id); diff --git a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs index a2ccbd30ac..215c79aaa4 100644 --- a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs @@ -8,7 +8,7 @@ /// /// Refreshes, clears, etc... any cache based on the information provided in the json /// - /// - void Refresh(string jsonPayload); + /// + void Refresh(string json); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs index e15fc09355..322e65654c 100644 --- a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs @@ -3,12 +3,12 @@ /// /// A cache refresher that supports refreshing cache based on a custom payload /// - interface IPayloadCacheRefresher : IJsonCacheRefresher + interface IPayloadCacheRefresher : IJsonCacheRefresher { /// /// Refreshes, clears, etc... any cache based on the information provided in the payload /// - /// - void Refresh(object payload); + /// + void Refresh(TPayload[] payloads); } } diff --git a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs index ff4276e158..efd3e37189 100644 --- a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs @@ -3,22 +3,27 @@ namespace Umbraco.Core.Cache { /// - /// Provides a base class for "json" cache refreshers. + /// A base class for "json" cache refreshers. /// - /// The actual cache refresher type. - /// Ensures that the correct events are raised when cache refreshing occurs. - public abstract class JsonCacheRefresherBase : CacheRefresherBase, IJsonCacheRefresher - where TInstance : ICacheRefresher + /// The actual cache refresher type. + /// The actual cache refresher type is used for strongly typed events. + public abstract class JsonCacheRefresherBase : CacheRefresherBase, IJsonCacheRefresher + where TInstanceType : class, ICacheRefresher { + /// + /// Initializes a new instance of the . + /// + /// A cache helper. protected JsonCacheRefresherBase(CacheHelper cacheHelper) : base(cacheHelper) - { - } + { } + /// + /// Refreshes as specified by a json payload. + /// + /// The json payload. public virtual void Refresh(string json) { OnCacheUpdated(Instance, new CacheRefresherEventArgs(json, MessageType.RefreshByJson)); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs index b34d49b729..52f95208a9 100644 --- a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs @@ -1,20 +1,39 @@ -using Umbraco.Core.Sync; +using Newtonsoft.Json; +using Umbraco.Core.Sync; namespace Umbraco.Core.Cache { /// - /// Provides a base class for "payload" cache refreshers. + /// A base class for "payload" class refreshers. /// - /// The actual cache refresher type. - /// Ensures that the correct events are raised when cache refreshing occurs. - public abstract class PayloadCacheRefresherBase : JsonCacheRefresherBase, IPayloadCacheRefresher - where TInstance : ICacheRefresher + /// The actual cache refresher type. + /// The payload type. + /// The actual cache refresher type is used for strongly typed events. + public abstract class PayloadCacheRefresherBase : JsonCacheRefresherBase, IPayloadCacheRefresher + where TInstanceType : class, ICacheRefresher { + /// + /// Initializes a new instance of the . + /// + /// A cache helper. protected PayloadCacheRefresherBase(CacheHelper cacheHelper) : base(cacheHelper) + { } + + #region Json + + /// + /// Deserializes a json payload into an object payload. + /// + /// The json payload. + /// The deserialized object payload. + protected virtual TPayload[] Deserialize(string json) { + return JsonConvert.DeserializeObject(json); } - protected abstract object Deserialize(string json); + #endregion + + #region Refresher public override void Refresh(string json) { @@ -22,9 +41,15 @@ namespace Umbraco.Core.Cache Refresh(payload); } - public virtual void Refresh(object payload) + /// + /// Refreshes as specified by a payload. + /// + /// The payload. + public virtual void Refresh(TPayload[] payloads) { - OnCacheUpdated(Instance, new CacheRefresherEventArgs(payload, MessageType.RefreshByPayload)); + OnCacheUpdated(Instance, new CacheRefresherEventArgs(payloads, MessageType.RefreshByPayload)); } + + #endregion } } diff --git a/src/Umbraco.Core/Cache/TypedCacheRefresherBase.cs b/src/Umbraco.Core/Cache/TypedCacheRefresherBase.cs index 36b7cdcd0e..c8277651a3 100644 --- a/src/Umbraco.Core/Cache/TypedCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/TypedCacheRefresherBase.cs @@ -3,17 +3,23 @@ namespace Umbraco.Core.Cache { /// - /// A base class for cache refreshers to inherit from that ensures the correct events are raised - /// when cache refreshing occurs. + /// A base class for "typed" cache refreshers. /// - /// The real cache refresher type, this is used for raising strongly typed events - /// The entity type that this refresher can update cache for + /// The actual cache refresher type. + /// The entity type. + /// The actual cache refresher type is used for strongly typed events. public abstract class TypedCacheRefresherBase : CacheRefresherBase, ICacheRefresher - where TInstanceType : ICacheRefresher + where TInstanceType : class, ICacheRefresher { - protected TypedCacheRefresherBase(CacheHelper cacheHelper) : base(cacheHelper) - { - } + /// + /// Initializes a new instance of the . + /// + /// A cache helper. + protected TypedCacheRefresherBase(CacheHelper cacheHelper) + : base(cacheHelper) + { } + + #region Refresher public virtual void Refresh(TEntityType instance) { @@ -24,5 +30,7 @@ namespace Umbraco.Core.Cache { OnCacheUpdated(Instance, new CacheRefresherEventArgs(instance, MessageType.RemoveByInstance)); } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 07a3307a77..4c90df9111 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment => "alpha0001"; + public static string CurrentComment => "alpha0002"; // Get the version of the Umbraco.Core.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 6fd3f01fe2..29c083b4a2 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -114,6 +114,11 @@ namespace Umbraco.Core /// public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; + /// + /// Guid for a Member object. + /// + public static readonly Guid MemberGuid = new Guid("39EB0F98-B348-42A1-8662-E7EB18487560"); + /// /// Guid for a Member Group object. /// diff --git a/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs b/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs index 8caaaf56ae..59edb78be7 100644 --- a/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs +++ b/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs @@ -18,10 +18,10 @@ namespace Umbraco.Core.DependencyInjection // boot manager when running in a web context container.Register(); - //the context + // register the service context container.RegisterSingleton(); - //now the services... + // register the services container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); diff --git a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs index c05800d2e0..bdda23ba3a 100644 --- a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs +++ b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs @@ -1,21 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using Umbraco.Core.Macros; namespace Umbraco.Core.Events { // Provides information on the macro that caused an error - public class MacroErrorEventArgs : System.EventArgs + public class MacroErrorEventArgs : EventArgs { /// - /// Name of the faulting macro. + /// Name of the faulting macro. /// public string Name { get; set; } /// - /// Alias of the faulting macro. + /// Alias of the faulting macro. /// public string Alias { get; set; } @@ -36,5 +33,10 @@ namespace Umbraco.Core.Events /// /// Macro error behaviour enum. public MacroErrorBehaviour Behaviour { get; set; } + + /// + /// The html code to display when Behavior is Content. + /// + public string Html { get; set; } } } diff --git a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs index be63c36d13..dd3d506b23 100644 --- a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs +++ b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs @@ -18,6 +18,12 @@ /// defined in Application_OnError. If no such error handler is defined /// then you'll see the Yellow Screen Of Death (YSOD) error page. /// - Throw + Throw, + + /// + /// Silently eat the error and display the custom content reported in + /// the error event args + /// + Content } } diff --git a/src/Umbraco.Core/Macros/PartialViewMacroResult.cs b/src/Umbraco.Core/Macros/PartialViewMacroResult.cs deleted file mode 100644 index bb0d61ac45..0000000000 --- a/src/Umbraco.Core/Macros/PartialViewMacroResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Umbraco.Core.Macros -{ - internal class PartialViewMacroResult - { - public PartialViewMacroResult() - { - } - - public PartialViewMacroResult(string result, Exception resultException) - { - Result = result; - ResultException = resultException; - } - - public string Result { get; set; } - public Exception ResultException { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeExtensions.cs b/src/Umbraco.Core/Models/ContentTypeExtensions.cs index ea6a28eb37..87cd881794 100644 --- a/src/Umbraco.Core/Models/ContentTypeExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeExtensions.cs @@ -7,31 +7,48 @@ namespace Umbraco.Core.Models internal static class ContentTypeExtensions { /// - /// Get all descendant content types + /// Gets all descendant content types of a specified content type. /// - /// - /// - /// - public static IEnumerable Descendants(this TItem contentType, IContentTypeServiceBase contentTypeService) + /// The content type. + /// The content type service. + /// The descendant content types. + /// Descendants corresponds to the parent-child relationship, and has + /// nothing to do with compositions, though a child should always be composed + /// of its parent. + public static IEnumerable Descendants(this TItem contentType, IContentTypeServiceBase contentTypeService) where TItem : IContentTypeComposition - { - var descendants = contentTypeService.GetChildren(contentType.Id) - .SelectRecursive(type => contentTypeService.GetChildren(type.Id)); - return descendants; + { + return contentTypeService.GetDescendants(contentType.Id, false); } /// - /// Get all descendant and self content types + /// Gets all descendant and self content types of a specified content type. /// - /// - /// - /// - public static IEnumerable DescendantsAndSelf(this TItem contentType, IContentTypeServiceBase contentTypeService) + /// The content type. + /// The content type service. + /// The descendant and self content types. + /// Descendants corresponds to the parent-child relationship, and has + /// nothing to do with compositions, though a child should always be composed + /// of its parent. + public static IEnumerable DescendantsAndSelf(this TItem contentType, IContentTypeServiceBase contentTypeService) where TItem : IContentTypeComposition { - var descendantsAndSelf = new[] { contentType }.Concat(contentType.Descendants(contentTypeService)); - return descendantsAndSelf; + return contentTypeService.GetDescendants(contentType.Id, true); + } + + /// + /// Gets all content types directly or indirectly composed of a specified content type. + /// + /// The content type. + /// The content type service. + /// The content types directly or indirectly composed of the content type. + /// This corresponds to the composition relationship and has nothing to do + /// with the parent-child relationship, though a child should always be composed of + /// its parent. + public static IEnumerable ComposedOf(this TItem contentType, IContentTypeServiceBase contentTypeService) + where TItem : IContentTypeComposition + { + return contentTypeService.GetComposedOf(contentType.Id); } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 9baf0c1024..5e83c99c90 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Cache; namespace Umbraco.Core.Models.PublishedContent { @@ -21,23 +20,51 @@ namespace Umbraco.Core.Models.PublishedContent // internal so it can be used by PublishedNoCache which does _not_ want to cache anything and so will never // use the static cache getter PublishedContentType.GetPublishedContentType(alias) below - anything else // should use it. - internal PublishedContentType(IContentTypeComposition contentType) + internal PublishedContentType(IContentType contentType) + : this(PublishedItemType.Content, contentType) + { } + + internal PublishedContentType(IMediaType mediaType) + : this(PublishedItemType.Media, mediaType) + { } + + internal PublishedContentType(IMemberType memberType) + : this(PublishedItemType.Member, memberType) + { } + + internal PublishedContentType(PublishedItemType itemType, IContentTypeComposition contentType) { Id = contentType.Id; Alias = contentType.Alias; + ItemType = itemType; _compositionAliases = new HashSet(contentType.CompositionAliases(), StringComparer.InvariantCultureIgnoreCase); - _propertyTypes = contentType.CompositionPropertyTypes - .Select(x => new PublishedPropertyType(this, x)) - .ToArray(); + var propertyTypes = contentType.CompositionPropertyTypes + .Select(x => new PublishedPropertyType(this, x)); + if (itemType == PublishedItemType.Member) + propertyTypes = WithMemberProperties(propertyTypes, this); + _propertyTypes = propertyTypes.ToArray(); InitializeIndexes(); } + // internal so it can be used for unit tests + internal PublishedContentType(int id, string alias, IEnumerable propertyTypes) + : this(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes) + { } + // internal so it can be used for unit tests internal PublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) + : this(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes) + { } + + // internal so it can be used for unit tests + internal PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes) { Id = id; Alias = alias; + ItemType = itemType; _compositionAliases = new HashSet(compositionAliases, StringComparer.InvariantCultureIgnoreCase); + if (itemType == PublishedItemType.Member) + propertyTypes = WithMemberProperties(propertyTypes); _propertyTypes = propertyTypes.ToArray(); foreach (var propertyType in _propertyTypes) propertyType.ContentType = this; @@ -59,20 +86,62 @@ namespace Umbraco.Core.Models.PublishedContent } } + // NOTE: code below defines and add custom, built-in, Umbraco properties for members + // unless they are already user-defined in the content type, then they are skipped + + // fixme should have constants for these + private const int TextboxDataTypeDefinitionId = -88; + //private const int BooleanDataTypeDefinitionId = -49; + //private const int DatetimeDataTypeDefinitionId = -36; + + static readonly Dictionary> BuiltinProperties = new Dictionary> + { + // fixme is this ok? + { "Email", Tuple.Create(TextboxDataTypeDefinitionId, Constants.PropertyEditors.TextboxAlias) }, + { "Username", Tuple.Create(TextboxDataTypeDefinitionId, Constants.PropertyEditors.TextboxAlias) }, + //{ "PasswordQuestion", Tuple.Create(TextboxDataTypeDefinitionId, Constants.PropertyEditors.TextboxAlias) }, + //{ "Comments", Tuple.Create(TextboxDataTypeDefinitionId, Constants.PropertyEditors.TextboxAlias) }, + //{ "IsApproved", Tuple.Create(BooleanDataTypeDefinitionId, Constants.PropertyEditors.BooleanEditorAlias) }, + //{ "IsLockedOut", Tuple.Create(BooleanDataTypeDefinitionId, Constants.PropertyEditors.BooleanEditorAlias) }, + //{ "LastLockoutDate", Tuple.Create(DatetimeDataTypeDefinitionId, Constants.PropertyEditors.DatetimeEditorAlias) }, + //{ "CreateDate", Tuple.Create(DatetimeDataTypeDefinitionId, Constants.PropertyEditors.DatetimeEditorAlias) }, + //{ "LastLoginDate", Tuple.Create(DatetimeDataTypeDefinitionId, Constants.PropertyEditors.DatetimeEditorAlias) }, + //{ "LastPasswordChangeDate", Tuple.Create(DatetimeDataTypeDefinitionId, Constants.PropertyEditors.DatetimeEditorAlias) }, + }; + + private static IEnumerable WithMemberProperties(IEnumerable propertyTypes, + PublishedContentType contentType = null) + { + var aliases = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var propertyType in propertyTypes) + { + aliases.Add(propertyType.PropertyTypeAlias); + yield return propertyType; + } + + foreach (var kvp in BuiltinProperties.Where(kvp => aliases.Contains(kvp.Key) == false)) + { + var propertyType = new PublishedPropertyType(kvp.Key, kvp.Value.Item1, kvp.Value.Item2, true); + if (contentType != null) propertyType.ContentType = contentType; + yield return propertyType; + } + } + #region Content type public int Id { get; private set; } + public string Alias { get; private set; } - public HashSet CompositionAliases { get { return _compositionAliases; } } + + public PublishedItemType ItemType { get; private set; } + + public HashSet CompositionAliases => _compositionAliases; #endregion #region Properties - public IEnumerable PropertyTypes - { - get { return _propertyTypes; } - } + public IEnumerable PropertyTypes => _propertyTypes; // alias is case-insensitive // this is the ONLY place where we compare ALIASES! @@ -98,108 +167,5 @@ namespace Umbraco.Core.Models.PublishedContent } #endregion - - #region Cache - - // these methods are called by ContentTypeCacheRefresher and DataTypeCacheRefresher - - internal static void ClearAll() - { - Logging.LogHelper.Debug("Clear all."); - // ok and faster to do it by types, assuming noone else caches PublishedContentType instances - //ApplicationContext.Current.ApplicationCache.ClearStaticCacheByKeySearch("PublishedContentType_"); - ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes(); - } - - internal static void ClearContentType(int id) - { - Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); - - // we don't support "get all" at the moment - so, cheating - var all = ApplicationContext.Current.ApplicationCache.StaticCache.GetCacheItemsByKeySearch("PublishedContentType_").ToArray(); - - // the one we want to clear - var clr = all.FirstOrDefault(x => x.Id == id); - if (clr == null) return; - - // those that have that one in their composition aliases - // note: CompositionAliases contains all recursive aliases - var oth = all.Where(x => x.CompositionAliases.InvariantContains(clr.Alias)).Select(x => x.Id); - - // merge ids - var ids = oth.Concat(new[] { clr.Id }).ToArray(); - - // clear them all at once - // we don't support "clear many at once" at the moment - so, cheating - ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( - (key, value) => ids.Contains(value.Id)); - } - - internal static void ClearDataType(int id) - { - Logging.LogHelper.Debug("Clear data type w/id {0}.", () => id); - // there is no recursion to handle here because a PublishedContentType contains *all* its - // properties ie both its own properties and those that were inherited (it's based upon an - // IContentTypeComposition) and so every PublishedContentType having a property based upon - // the cleared data type, be it local or inherited, will be cleared. - ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( - (key, value) => value.PropertyTypes.Any(x => x.DataTypeId == id)); - } - - public static PublishedContentType Get(PublishedItemType itemType, string alias) - { - var key = string.Format("PublishedContentType_{0}_{1}", - itemType.ToString().ToLowerInvariant(), alias.ToLowerInvariant()); - - var type = ApplicationContext.Current.ApplicationCache.StaticCache.GetCacheItem(key, - () => CreatePublishedContentType(itemType, alias)); - - return type; - } - - private static PublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) - { - if (GetPublishedContentTypeCallback != null) - return GetPublishedContentTypeCallback(alias); - - IContentTypeComposition contentType; - switch (itemType) - { - case PublishedItemType.Content: - contentType = ApplicationContext.Current.Services.ContentTypeService.Get(alias); - break; - case PublishedItemType.Media: - contentType = ApplicationContext.Current.Services.MediaTypeService.Get(alias); - break; - case PublishedItemType.Member: - contentType = ApplicationContext.Current.Services.MemberTypeService.Get(alias); - break; - default: - throw new ArgumentOutOfRangeException("itemType"); - } - - if (contentType == null) - throw new Exception(string.Format("ContentTypeService failed to find a {0} type with alias \"{1}\".", - itemType.ToString().ToLower(), alias)); - - return new PublishedContentType(contentType); - } - - // for unit tests - changing the callback must reset the cache obviously - private static Func _getPublishedContentTypeCallBack; - internal static Func GetPublishedContentTypeCallback - { - get { return _getPublishedContentTypeCallBack; } - set - { - // see note above - //ClearAll(); - ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheByKeySearch("PublishedContentType_"); - - _getPublishedContentTypeCallBack = value; - } - } - - #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index f27b827cc3..b17c8a7dbb 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -107,14 +106,15 @@ namespace Umbraco.Core.Models.PublishedContent /// The property type alias. /// The datatype definition identifier. /// The property editor alias. + /// A value indicating whether the property is an Umbraco-defined property. /// /// The new published property type does not belong to a published content type. /// The values of and are /// assumed to be valid and consistent. /// - internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias) + internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias, bool umbraco = false) { - // ContentType + // ContentType // - in unit tests, to be set by PublishedContentType when creating it // - in detached types, remains null @@ -122,6 +122,7 @@ namespace Umbraco.Core.Models.PublishedContent DataTypeId = dataTypeDefinitionId; PropertyEditorAlias = propertyEditorAlias; + IsUmbraco = umbraco; InitializeConverters(); } @@ -139,17 +140,22 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets or sets the alias uniquely identifying the property type. /// - public string PropertyTypeAlias { get; private set; } + public string PropertyTypeAlias { get; } /// /// Gets or sets the identifier uniquely identifying the data type supporting the property type. /// - public int DataTypeId { get; private set; } + public int DataTypeId { get; } /// /// Gets or sets the alias uniquely identifying the property editor for the property type. /// - public string PropertyEditorAlias { get; private set; } + public string PropertyEditorAlias { get; } + + /// + /// Gets or sets a value indicating whether the property is an Umbraco-defined property. + /// + internal bool IsUmbraco { get; private set; } #endregion @@ -168,11 +174,11 @@ namespace Umbraco.Core.Models.PublishedContent //TODO: Look at optimizing this method, it gets run for every property type for the document being rendered at startup, // every precious second counts! - var converters = PropertyValueConvertersResolver.Current.Converters.ToArray(); + var converters = PropertyValueConvertersResolver.Current.Converters.ToArray(); var defaultConvertersWithAttributes = PropertyValueConvertersResolver.Current.DefaultConverters; _converter = null; - + //get all converters for this property type var foundConverters = converters.Where(x => x.IsConverter(this)).ToArray(); if (foundConverters.Length == 1) @@ -199,7 +205,7 @@ namespace Umbraco.Core.Models.PublishedContent ContentType.Alias, PropertyTypeAlias, nonDefault[1].GetType().FullName, nonDefault[0].GetType().FullName)); } - else + else { //we need to remove any converters that have been shadowed by another converter var foundDefaultConvertersWithAttributes = defaultConvertersWithAttributes.Where(x => foundConverters.Contains(x.Item1)); @@ -210,7 +216,7 @@ namespace Umbraco.Core.Models.PublishedContent if (nonShadowedDefaultConverters.Length == 1) { //assign to the single default converter - _converter = nonShadowedDefaultConverters[0]; + _converter = nonShadowedDefaultConverters[0]; } else if (nonShadowedDefaultConverters.Length > 1) { @@ -223,7 +229,7 @@ namespace Umbraco.Core.Models.PublishedContent nonShadowedDefaultConverters[1].GetType().FullName, nonShadowedDefaultConverters[0].GetType().FullName)); } } - + } var converterMeta = _converter as IPropertyValueConverterMeta; @@ -268,9 +274,9 @@ namespace Umbraco.Core.Models.PublishedContent var attr = converter.GetType().GetCustomAttributes(false) .FirstOrDefault(x => x.Value == value || x.Value == PropertyCacheValue.All); - return attr == null ? PropertyCacheLevel.Request : attr.Level; + return attr?.Level ?? PropertyCacheLevel.Request; } - + // converts the raw value into the source value // uses converters, else falls back to dark (& performance-wise expensive) magic // source: the property raw value @@ -278,13 +284,13 @@ namespace Umbraco.Core.Models.PublishedContent public object ConvertDataToSource(object source, bool preview) { // use the converter else use dark (& performance-wise expensive) magic - return _converter != null - ? _converter.ConvertDataToSource(this, source, preview) + return _converter != null + ? _converter.ConvertDataToSource(this, source, preview) : ConvertUsingDarkMagic(source); } // gets the source cache level - public PropertyCacheLevel SourceCacheLevel { get { return _sourceCacheLevel; } } + public PropertyCacheLevel SourceCacheLevel => _sourceCacheLevel; // converts the source value into the clr value // uses converters, else returns the source value @@ -295,12 +301,12 @@ namespace Umbraco.Core.Models.PublishedContent // use the converter if any // else just return the source value return _converter != null - ? _converter.ConvertSourceToObject(this, source, preview) + ? _converter.ConvertSourceToObject(this, source, preview) : source; } // gets the value cache level - public PropertyCacheLevel ObjectCacheLevel { get { return _objectCacheLevel; } } + public PropertyCacheLevel ObjectCacheLevel => _objectCacheLevel; // converts the source value into the xpath value // uses the converter else returns the source value as a string @@ -322,7 +328,7 @@ namespace Umbraco.Core.Models.PublishedContent } // gets the xpath cache level - public PropertyCacheLevel XPathCacheLevel { get { return _xpathCacheLevel; } } + public PropertyCacheLevel XPathCacheLevel => _xpathCacheLevel; internal static object ConvertUsingDarkMagic(object source) { @@ -356,23 +362,17 @@ namespace Umbraco.Core.Models.PublishedContent } // gets the property CLR type - public Type ClrType { get { return _clrType; } } + public Type ClrType => _clrType; #endregion - - #region Detached private PropertyCacheLevel _sourceCacheLevelReduced = 0; private PropertyCacheLevel _objectCacheLevelReduced = 0; private PropertyCacheLevel _xpathCacheLevelReduced = 0; - internal bool IsDetachedOrNested - { - // enough to test source - get { return _sourceCacheLevelReduced != 0; } - } + internal bool IsDetachedOrNested => _sourceCacheLevelReduced != 0; /// /// Creates a detached clone of this published property type. @@ -389,13 +389,13 @@ namespace Umbraco.Core.Models.PublishedContent throw new Exception("PublishedPropertyType is already detached/nested."); var detached = new PublishedPropertyType(this); - detached._sourceCacheLevel - = detached._objectCacheLevel - = detached._xpathCacheLevel + detached._sourceCacheLevel + = detached._objectCacheLevel + = detached._xpathCacheLevel = PropertyCacheLevel.Content; // set to none to a) indicate it's detached / nested and b) make sure any nested // types switch all their cache to .Content - detached._sourceCacheLevelReduced + detached._sourceCacheLevelReduced = detached._objectCacheLevelReduced = detached._xpathCacheLevelReduced = PropertyCacheLevel.None; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs new file mode 100644 index 0000000000..a3f8fa63a3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight +{ + [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + public class RefactorXmlColumns : MigrationBase + { + public RefactorXmlColumns(ILogger logger) + : base(logger) + { } + + public override void Up() + { + if (ColumnExists("cmsContentXml", "Rv") == false) + Alter.Table("cmsContentXml").AddColumn("Rv").AsInt64().NotNullable().WithDefaultValue(0); + + if (ColumnExists("cmsPreviewXml", "Rv") == false) + Alter.Table("cmsPreviewXml").AddColumn("Rv").AsInt64().NotNullable().WithDefaultValue(0); + + // remove the any PK_ and the FK_ to cmsContentVersion.VersionId + if (DatabaseType.IsMySql()) + { + Delete.PrimaryKey("PK_cmsPreviewXml").FromTable("cmsPreviewXml"); + + Delete.ForeignKey().FromTable("cmsPreviewXml").ForeignColumn("VersionId") + .ToTable("cmsContentVersion").PrimaryColumn("VersionId"); + } + else + { + var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); + var dups = new List(); + foreach (var c in constraints.Where(x => x.Item1.InvariantEquals("cmsPreviewXml") && x.Item3.InvariantStartsWith("PK_"))) + { + var keyName = c.Item3.ToLowerInvariant(); + if (dups.Contains(keyName)) + { + Logger.Warn("Duplicate constraint " + c.Item3); + continue; + } + dups.Add(keyName); + Delete.PrimaryKey(c.Item3).FromTable(c.Item1); + } + foreach (var c in constraints.Where(x => x.Item1.InvariantEquals("cmsPreviewXml") && x.Item3.InvariantStartsWith("FK_cmsPreviewXml_cmsContentVersion"))) + { + Delete.ForeignKey().FromTable("cmsPreviewXml").ForeignColumn("VersionId") + .ToTable("cmsContentVersion").PrimaryColumn("VersionId"); + } + } + + if (ColumnExists("cmsPreviewXml", "Timestamp")) + Delete.Column("Timestamp").FromTable("cmsPreviewXml"); + + if (ColumnExists("cmsPreviewXml", "VersionId")) + { + RemoveDuplicates(); + Delete.Column("VersionId").FromTable("cmsPreviewXml"); + } + + // re-create the primary key + Create.PrimaryKey("PK_cmsPreviewXml") + .OnTable("cmsPreviewXml") + .Columns(new[] { "nodeId" }); + } + + public override void Down() + { + throw new DataLossException("Downgrading is not supported."); + + //if (Exists("cmsContentXml", "Rv")) + // Delete.Column("Rv").FromTable("cmsContentXml"); + //if (Exists("cmsPreviewXml", "Rv")) + // Delete.Column("Rv").FromTable("cmsContentXml"); + } + + private bool ColumnExists(string tableName, string columnName) + { + // that's ok even on MySql + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + return columns.Any(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); + } + + private void RemoveDuplicates() + { + const string sql = @"delete from cmsPreviewXml where versionId in ( +select cmsPreviewXml.versionId from cmsPreviewXml +join cmsDocument on cmsPreviewXml.versionId=cmsDocument.versionId +where cmsDocument.newest <> 1)"; + + Context.Database.Execute(sql); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index 0689117d9b..a85e865c49 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; @@ -24,5 +25,12 @@ namespace Umbraco.Core.Persistence.Querying /// IEnumerable> GetWhereClauses(); + /// + /// Adds a where-in clause to the query + /// + /// + /// + /// This instance so calls to this method are chainable + IQuery WhereIn(Expression> fieldSelector, IEnumerable values); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/Query.cs b/src/Umbraco.Core/Persistence/Querying/Query.cs index c51b451c21..4c58e9e122 100644 --- a/src/Umbraco.Core/Persistence/Querying/Query.cs +++ b/src/Umbraco.Core/Persistence/Querying/Query.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -41,6 +42,18 @@ namespace Umbraco.Core.Persistence.Querying return this; } + public virtual IQuery WhereIn(Expression> fieldSelector, IEnumerable values) + { + if (fieldSelector != null) + { + var expressionHelper = new ModelToSqlExpressionHelper(_sqlSyntax, _mappingResolver); + string whereExpression = expressionHelper.Visit(fieldSelector); + + _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { @values = values } })); + } + return this; + } + /// /// Returns all translated where clauses and their sql parameters /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 17e6664e35..ff85fff35f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -24,14 +24,12 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Represents a repository for doing CRUD operations for . /// - internal class ContentRepository : RecycleBinRepository, IContentRepository + internal class ContentRepository : RecycleBinRepository, IContentRepository { private readonly IContentTypeRepository _contentTypeRepository; private readonly ITemplateRepository _templateRepository; private readonly ITagRepository _tagRepository; private readonly CacheHelper _cacheHelper; - private readonly ContentPreviewRepository _contentPreviewRepository; - private readonly ContentXmlRepository _contentXmlRepository; public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cacheHelper, logger, contentSection, mappingResolver) @@ -43,12 +41,12 @@ namespace Umbraco.Core.Persistence.Repositories _templateRepository = templateRepository; _tagRepository = tagRepository; _cacheHelper = cacheHelper; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); EnsureUniqueNaming = true; } + protected override ContentRepository Instance => this; + public bool EnsureUniqueNaming { get; set; } #region Overrides of RepositoryBase @@ -233,7 +231,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PerformDeleteVersion(int id, Guid versionId) { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + // raise event first else potential FK issues + OnUowRemovingVersion(new UnitOfWorkVersionEventArgs(UnitOfWork, id, versionId)); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); @@ -245,6 +245,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistDeletedItem(IContent entity) { + // raise event first else potential FK issues + OnUowRemovingEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + //We need to clear out all access rules but we need to do this in a manual way since // nothing in that table is joined to a content id var subQuery = Sql() @@ -372,6 +375,8 @@ namespace Umbraco.Core.Persistence.Repositories ((Content)entity).PublishedVersionGuid = dto.VersionId; } + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + entity.ResetDirtyProperties(); } @@ -551,6 +556,8 @@ namespace Umbraco.Core.Persistence.Repositories content.PublishedVersionGuid = default(Guid); } + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + entity.ResetDirtyProperties(); } @@ -682,16 +689,16 @@ namespace Umbraco.Core.Persistence.Repositories /// /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true) { + var filterSql = Sql(); + if (newest) + filterSql.Append("AND (cmsDocument.newest = 1)"); - var filterSql = Sql().Append("AND (cmsDocument.newest = 1)"); if (filter != null) { foreach (var filterClaus in filter.GetWhereClauses()) - { filterSql.Append($"AND ({filterClaus.Item1})", filterClaus.Item2); - } } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, @@ -896,151 +903,5 @@ WHERE (@path LIKE {5})", return currentName; } - - #region Xml - Should Move! - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var subQuery = Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Where(x => x.Published); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.Published && x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - long total; - var processed = 0; - do - { - //NOTE: This is an important call, we cannot simply make a call to: - // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - // because that method is used to query 'latest' content items where in this case we don't necessarily - // want latest content items because a pulished content item might not actually be the latest. - // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - MapQueryDtos, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - public XElement GetContentXml(int contentId) - { - var sql = Sql().SelectAll().From().Where(d => d.NodeId == contentId); - var dto = Database.SingleOrDefault(sql); - if (dto == null) return null; - return XElement.Parse(dto.Xml); - } - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - public XElement GetContentPreviewXml(int contentId, Guid version) - { - var sql = Sql().SelectAll().From() - .Where(d => d.NodeId == contentId && d.VersionId == version); - var dto = Database.SingleOrDefault(sql); - if (dto == null) return null; - return XElement.Parse(dto.Xml); - } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 676fd4db2a..11a98c678e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -56,26 +56,6 @@ namespace Umbraco.Core.Persistence.Repositories /// IEnumerable GetPermissionsForEntity(int entityId); - /// - /// Used to add/update published xml for the content item - /// - /// - /// - void AddOrUpdateContentXml(IContent content, Func xml); - - /// - /// Used to remove the content xml for a content item - /// - /// - void DeleteContentXml(IContent content); - - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IContent content, Func xml); - /// /// Gets paged content results /// @@ -87,23 +67,9 @@ namespace Umbraco.Core.Persistence.Repositories /// Direction to order by /// Flag to indicate when ordering by system field /// + /// A value indicating whether to get the 'newest' or all of them. /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null); - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - XElement GetContentXml(int contentId); - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - XElement GetContentPreviewXml(int contentId, Guid version); + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 2e165a6ed6..e42827a350 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -11,20 +11,6 @@ namespace Umbraco.Core.Persistence.Repositories { IMedia GetMediaByPath(string mediaPath); - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMedia content, Func xml); - - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); - /// /// Gets paged media results /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index a24116f0e2..2cab1dc69c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -59,20 +59,5 @@ namespace Umbraco.Core.Persistence.Repositories //IEnumerable GetPagedResultsByQuery( // Sql sql, int pageIndex, int pageSize, out int totalRecords, // Func, int[]> resolveIds); - - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMember content, Func xml); - - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMember content, Func xml); - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs index a6ed95711e..ce6ed2561a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs @@ -12,12 +12,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetEntitiesInRecycleBin(); - - /// - /// Called to empty the recycle bin - /// - /// - bool EmptyRecycleBin(); - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index 229a6fc0ef..6aa9336377 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -13,15 +13,6 @@ namespace Umbraco.Core.Persistence.Repositories public interface IRepositoryVersionable : IRepositoryQueryable where TEntity : IAggregateRoot { - /// - /// Rebuilds the xml structures for all TEntity if no content type ids are specified, otherwise rebuilds the xml structures - /// for only the content types specified - /// - /// The serializer to convert TEntity to Xml - /// Structures will be rebuilt in chunks of this size - /// - void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null); - /// /// Get the total count of entities /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index b97283542e..1fac280f4e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -24,12 +24,10 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Represents a repository for doing CRUD operations for /// - internal class MediaRepository : RecycleBinRepository, IMediaRepository + internal class MediaRepository : RecycleBinRepository, IMediaRepository { private readonly IMediaTypeRepository _mediaTypeRepository; private readonly ITagRepository _tagRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cache, logger, contentSection, mappingResolver) @@ -38,11 +36,11 @@ namespace Umbraco.Core.Persistence.Repositories if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); _mediaTypeRepository = mediaTypeRepository; _tagRepository = tagRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } + protected override MediaRepository Instance => this; + public bool EnsureUniqueNaming { get; } #region Overrides of RepositoryBase @@ -126,9 +124,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", "DELETE FROM cmsDocument WHERE nodeId = @Id", "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoNode WHERE id = @Id" }; @@ -204,7 +200,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PerformDeleteVersion(int id, Guid versionId) { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + // raise event first else potential FK issues + OnUowRemovingVersion(new UnitOfWorkVersionEventArgs(UnitOfWork, id, versionId)); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); } @@ -282,6 +280,8 @@ namespace Umbraco.Core.Persistence.Repositories UpdateEntityTags(entity, _tagRepository); + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + entity.ResetDirtyProperties(); } @@ -364,9 +364,18 @@ namespace Umbraco.Core.Persistence.Repositories UpdateEntityTags(entity, _tagRepository); + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + entity.ResetDirtyProperties(); } + protected override void PersistDeletedItem(IMedia entity) + { + // raise event first else potential FK issues + OnUowRemovingEntity(new UnitOfWorkEntityEventArgs(this.UnitOfWork, entity)); + base.PersistDeletedItem(entity); + } + #endregion #region IRecycleBinRepository members @@ -520,103 +529,5 @@ namespace Umbraco.Core.Persistence.Repositories return currentName; } - - #region Xml - Should Move! - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - RebuildXmlStructuresProcessQuery(serializer, Query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - long total; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - - public void AddOrUpdateContentXml(IMedia content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMedia content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 124832b08e..c0b5d1bf83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -23,13 +23,11 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Represents a repository for doing CRUD operations for /// - internal class MemberRepository : VersionableRepositoryBase, IMemberRepository + internal class MemberRepository : VersionableRepositoryBase, IMemberRepository { private readonly IMemberTypeRepository _memberTypeRepository; private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cache, logger, contentSection, mappingResolver) @@ -39,10 +37,10 @@ namespace Umbraco.Core.Persistence.Repositories _memberTypeRepository = memberTypeRepository; _tagRepository = tagRepository; _memberGroupRepository = memberGroupRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); } + protected override MemberRepository Instance => this; + #region Overrides of RepositoryBase protected override IMember PerformGet(int id) @@ -260,6 +258,8 @@ namespace Umbraco.Core.Persistence.Repositories UpdateEntityTags(entity, _tagRepository); + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + ((Member)entity).ResetDirtyProperties(); } @@ -373,9 +373,18 @@ namespace Umbraco.Core.Persistence.Repositories UpdateEntityTags(entity, _tagRepository); + OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + dirtyEntity.ResetDirtyProperties(); } + protected override void PersistDeletedItem(IMember entity) + { + // raise event first else potential FK issues + OnUowRemovingEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); + base.PersistDeletedItem(entity); + } + #endregion #region Overrides of VersionableRepositoryBase @@ -409,7 +418,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PerformDeleteVersion(int id, Guid versionId) { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + // raise event first else potential FK issues + OnUowRemovingVersion(new UnitOfWorkVersionEventArgs(UnitOfWork, id, versionId)); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); } @@ -636,103 +647,5 @@ namespace Umbraco.Core.Persistence.Repositories ((Entity)member).ResetDirtyProperties(false); return member; } - - #region Xml - Should Move! - - public void AddOrUpdateContentXml(IMember content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMember content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - long total; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index 0be15ede71..298fa915d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -10,8 +10,9 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - internal abstract class RecycleBinRepository : VersionableRepositoryBase, IRecycleBinRepository + internal abstract class RecycleBinRepository : VersionableRepositoryBase, IRecycleBinRepository where TEntity : class, IUmbracoEntity + where TRepository : class, IRepository { protected RecycleBinRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cache, logger, contentSection, mappingResolver) @@ -25,74 +26,6 @@ namespace Umbraco.Core.Persistence.Repositories return GetByQuery(Query.Where(entity => entity.Trashed)); } - /// - /// Empties the Recycle Bin by running single bulk-Delete queries - /// against the Content- or Media's Recycle Bin. - /// - /// - public virtual bool EmptyRecycleBin() - { - var db = this.Database; - - //Construct and execute delete statements for all trashed items by 'nodeObjectType' - var deletes = new List - { - FormatDeleteStatement("umbracoUser2NodeNotify", "nodeId"), - FormatDeleteStatement("umbracoUser2NodePermission", "nodeId"), - @"DELETE FROM umbracoAccessRule WHERE umbracoAccessRule.accessId IN ( - SELECT TB1.id FROM umbracoAccess as TB1 - INNER JOIN umbracoNode as TB2 ON TB1.nodeId = TB2.id - WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", - FormatDeleteStatement("umbracoAccess", "nodeId"), - FormatDeleteStatement("umbracoRelation", "parentId"), - FormatDeleteStatement("umbracoRelation", "childId"), - FormatDeleteStatement("cmsTagRelationship", "nodeId"), - FormatDeleteStatement("umbracoDomains", "domainRootStructureID"), - FormatDeleteStatement("cmsDocument", "nodeId"), - FormatDeleteStatement("cmsPropertyData", "contentNodeId"), - FormatDeleteStatement("cmsPreviewXml", "nodeId"), - FormatDeleteStatement("cmsContentVersion", "ContentId"), - FormatDeleteStatement("cmsContentXml", "nodeId"), - FormatDeleteStatement("cmsContent", "nodeId"), - "UPDATE umbracoNode SET parentID = '" + RecycleBinId + "' WHERE trashed = '1' AND nodeObjectType = @NodeObjectType", - "DELETE FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType" - }; - - //Wraps in transaction - this improves performance and also ensures - // that if any of the deletions fails that the whole thing is rolled back. - using (var trans = db.GetTransaction()) - { - try - { - foreach (var delete in deletes) - { - db.Execute(delete, new { NodeObjectType = NodeObjectTypeId }); - } - - trans.Complete(); - - return true; - } - catch (Exception ex) - { - // transaction will rollback - Logger.Error>("An error occurred while emptying the Recycle Bin: " + ex.Message, ex); - throw; - } - } - } - - private string FormatDeleteStatement(string tableName, string keyName) - { - //This query works with sql ce and sql server: - //DELETE FROM umbracoUser2NodeNotify WHERE umbracoUser2NodeNotify.nodeId IN - //(SELECT nodeId FROM umbracoUser2NodeNotify as TB1 INNER JOIN umbracoNode as TB2 ON TB1.nodeId = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = 'C66BA18E-EAF3-4CFF-8A22-41B16D66A972') - return - string.Format( - "DELETE FROM {0} WHERE {0}.{1} IN (SELECT TB1.{1} FROM {0} as TB1 INNER JOIN umbracoNode as TB2 ON TB1.{1} = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", - tableName, keyName); - } - /// /// Gets a list of files, which are referenced on items in the Recycle Bin. /// The list is generated by the convention that a file is referenced by diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 1036b46d13..45a9249d6a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -22,7 +20,6 @@ using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; -using Umbraco.Core.Dynamics; using Umbraco.Core.IO; using Umbraco.Core.Persistence.Mappers; @@ -48,8 +45,9 @@ namespace Umbraco.Core.Persistence.Repositories } } - internal abstract class VersionableRepositoryBase : NPocoRepositoryBase + internal abstract class VersionableRepositoryBase : NPocoRepositoryBase where TEntity : class, IAggregateRoot + where TRepository : class, IRepository { private readonly IContentSection _contentSection; @@ -59,6 +57,8 @@ namespace Umbraco.Core.Persistence.Repositories _contentSection = contentSection; } + protected abstract TRepository Instance { get; } + #region IRepositoryVersionable Implementation public virtual IEnumerable GetAllVersions(int id) @@ -103,10 +103,10 @@ namespace Umbraco.Core.Persistence.Repositories var list = Database.Fetch( "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", - new {VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); + new { /*VersionId =*/ latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); if (list.Any() == false) return; - using (var transaction = Database.GetTransaction()) + using (var transaction = Database.GetTransaction()) // fixme - though... already in a unit of work? { foreach (var dto in list) { @@ -479,7 +479,7 @@ namespace Umbraco.Core.Persistence.Repositories if (result.ContainsKey(def.Id)) { - Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); + Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); } result[def.Id] = new PropertyCollection(properties); } @@ -541,7 +541,60 @@ namespace Umbraco.Core.Persistence.Repositories return SqlSyntax.GetQuotedTableName(tableName) + "." + SqlSyntax.GetQuotedColumnName(fieldName); } + #region UnitOfWork Events + + public class UnitOfWorkEntityEventArgs : EventArgs + { + public UnitOfWorkEntityEventArgs(IDatabaseUnitOfWork unitOfWork, TEntity entity) + { + UnitOfWork = unitOfWork; + Entity = entity; + } + + public IDatabaseUnitOfWork UnitOfWork { get; } + + public TEntity Entity { get; } + } + + public class UnitOfWorkVersionEventArgs : EventArgs + { + public UnitOfWorkVersionEventArgs(IDatabaseUnitOfWork unitOfWork, int entityId, Guid versionId) + { + UnitOfWork = unitOfWork; + EntityId = entityId; + VersionId = versionId; + } + + public IDatabaseUnitOfWork UnitOfWork { get; private set; } + + public int EntityId { get; } + + public Guid VersionId { get; } + } + + public static event TypedEventHandler UowRefreshedEntity; + public static event TypedEventHandler UowRemovingEntity; + public static event TypedEventHandler UowRemovingVersion; + + protected void OnUowRefreshedEntity(UnitOfWorkEntityEventArgs args) + { + UowRefreshedEntity.RaiseEvent(args, Instance); + } + + protected void OnUowRemovingEntity(UnitOfWorkEntityEventArgs args) + { + UowRemovingEntity.RaiseEvent(args, Instance); + } + + protected void OnUowRemovingVersion(UnitOfWorkVersionEventArgs args) + { + UowRemovingVersion.RaiseEvent(args, Instance); + } + + #endregion + /// + /// /// Deletes all media files passed in. /// /// @@ -578,7 +631,7 @@ namespace Umbraco.Core.Persistence.Repositories } catch (Exception e) { - Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); + Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); allsuccess = false; } }); diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs new file mode 100644 index 0000000000..a5182f0829 --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services.Changes +{ + internal class ContentTypeChange + where TItem : class, IContentTypeComposition + { + public ContentTypeChange(TItem item, ContentTypeChangeTypes changeTypes) + { + Item = item; + ChangeTypes = changeTypes; + } + + public TItem Item { get; } + + public ContentTypeChangeTypes ChangeTypes { get; internal set; } + + public EventArgs ToEventArgs(ContentTypeChange change) + { + return new EventArgs(change); + } + + public class EventArgs : System.EventArgs + { + public EventArgs(IEnumerable> changes) + { + Changes = changes.ToArray(); + } + + public EventArgs(ContentTypeChange change) + : this(new[] { change }) + { } + + public IEnumerable> Changes { get; private set; } + } + } + +} diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs new file mode 100644 index 0000000000..f783098db0 --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services.Changes +{ + internal static class ContentTypeChangeExtensions + { + public static ContentTypeChange.EventArgs ToEventArgs(this IEnumerable> changes) + where TItem : class, IContentTypeComposition + { + return new ContentTypeChange.EventArgs(changes); + } + + public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type) + { + return (change & type) != ContentTypeChangeTypes.None; + } + + public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) + { + return (change & types) == types; + } + + public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) + { + return (change & types) != ContentTypeChangeTypes.None; + } + + public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) + { + return (change & types) == ContentTypeChangeTypes.None; + } + } +} diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs new file mode 100644 index 0000000000..a39cd4617b --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Core.Services.Changes +{ + [Flags] + public enum ContentTypeChangeTypes : byte + { + None = 0, + RefreshMain = 1, // changed, impacts content (adding ppty or composition does NOT) + RefreshOther = 2, // changed, other changes + Remove = 4 // item type has been removed + } +} diff --git a/src/Umbraco.Core/Services/Changes/TreeChange.cs b/src/Umbraco.Core/Services/Changes/TreeChange.cs new file mode 100644 index 0000000000..81c9b67c3f --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/TreeChange.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Services.Changes +{ + internal class TreeChange + { + public TreeChange(TItem changedItem, TreeChangeTypes changeTypes) + { + Item = changedItem; + ChangeTypes = changeTypes; + } + + public TItem Item { get; } + public TreeChangeTypes ChangeTypes { get; } + + public EventArgs ToEventArgs() + { + return new EventArgs(this); + } + + public class EventArgs : System.EventArgs + { + public EventArgs(IEnumerable> changes) + { + Changes = changes.ToArray(); + } + + public EventArgs(TreeChange change) + : this(new[] { change }) + { } + + public IEnumerable> Changes { get; private set; } + } + } +} diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs new file mode 100644 index 0000000000..e8c1db3f44 --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Services.Changes +{ + internal static class TreeChangeExtensions + { + public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) + { + return new TreeChange.EventArgs(changes); + } + + public static bool HasType(this TreeChangeTypes change, TreeChangeTypes type) + { + return (change & type) != TreeChangeTypes.None; + } + + public static bool HasTypesAll(this TreeChangeTypes change, TreeChangeTypes types) + { + return (change & types) == types; + } + + public static bool HasTypesAny(this TreeChangeTypes change, TreeChangeTypes types) + { + return (change & types) != TreeChangeTypes.None; + } + + public static bool HasTypesNone(this TreeChangeTypes change, TreeChangeTypes types) + { + return (change & types) == TreeChangeTypes.None; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs new file mode 100644 index 0000000000..34d5f39b65 --- /dev/null +++ b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs @@ -0,0 +1,25 @@ +using System; + +namespace Umbraco.Core.Services.Changes +{ + [Flags] + public enum TreeChangeTypes : byte + { + None = 0, + + // all items have been refreshed + RefreshAll = 1, + + // an item node has been refreshed + // with only local impact + RefreshNode = 2, + + // an item node has been refreshed + // with branch impact + RefreshBranch = 4, + + // an item node has been removed + // never to return + Remove = 8, + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c2f803e945..22b34dcdb5 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; namespace Umbraco.Core.Services @@ -21,10 +22,6 @@ namespace Umbraco.Core.Services /// public class ContentService : RepositoryService, IContentService, IContentServiceOperations { - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - private readonly IEnumerable _urlSegmentProviders; private IContentTypeService _contentTypeService; #region Constructors @@ -32,18 +29,9 @@ namespace Umbraco.Core.Services public ContentService( IDatabaseUnitOfWorkProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IDataTypeService dataTypeService, - IUserService userService, - IEnumerable urlSegmentProviders) + IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) { - if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); - if (userService == null) throw new ArgumentNullException(nameof(userService)); - if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); - _dataTypeService = dataTypeService; - _userService = userService; - _urlSegmentProviders = urlSegmentProviders; } // don't change or remove this, will need it later @@ -343,9 +331,9 @@ namespace Umbraco.Core.Services var repo = uow.CreateRepository(); repo.AddOrUpdate(content); - repo.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); Saved.RaiseEvent(new SaveEventArgs(content, false), this); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs(), this); } Created.RaiseEvent(new NewEventArgs(content, false, content.ContentType.Alias, parent), this); @@ -591,7 +579,7 @@ namespace Umbraco.Core.Services { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var filterQuery = filter.IsNullOrWhiteSpace() + var filterQuery = filter.IsNullOrWhiteSpace() ? null : repository.QueryFactory.Create().Where(x => x.Name.Contains(filter)); return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); @@ -964,48 +952,6 @@ namespace Umbraco.Core.Services #region Save, Publish, Unpublish - /// - /// This will rebuild the xml structures for content in the database. - /// - /// This is not used for anything - /// True if publishing succeeded, otherwise False - /// - /// This is used for when a document type alias or a document type property is changed, the xml will need to - /// be regenerated. - /// - public bool RePublishAll(int userId = 0) - { - try - { - RebuildXmlStructures(); - return true; - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - return false; - } - } - - /// - /// This will rebuild the xml structures for content in the database. - /// - /// - /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure - /// for all published content. - /// - internal void RePublishAll(params int[] contentTypeIds) - { - try - { - RebuildXmlStructures(contentTypeIds); - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - } - } - /// /// Saves a single object /// @@ -1030,6 +976,8 @@ namespace Umbraco.Core.Services if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); + var isNew = content.IsNewEntity(); + using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); @@ -1045,14 +993,14 @@ namespace Umbraco.Core.Services content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); return OperationStatus.Attempt.Succeed(evtMsgs); @@ -1087,6 +1035,9 @@ namespace Umbraco.Core.Services if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(contentsA, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); + var treeChanges = contentsA.Select(x => new TreeChange(x, + x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); @@ -1103,7 +1054,6 @@ namespace Umbraco.Core.Services content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } uow.Complete(); @@ -1111,6 +1061,7 @@ namespace Umbraco.Core.Services if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(contentsA, false, evtMsgs), this); + TreeChanged.RaiseEvent(treeChanges.ToEventArgs(), this); Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); return OperationStatus.Attempt.Succeed(evtMsgs); @@ -1348,6 +1299,7 @@ namespace Umbraco.Core.Services DeleteLocked(repository, content); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs(), this); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); @@ -1491,6 +1443,7 @@ namespace Umbraco.Core.Services PerformMoveLocked(repository, content, Constants.System.RecycleBinContent, null, userId, moves, true); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } var moveInfo = moves @@ -1555,6 +1508,7 @@ namespace Umbraco.Core.Services PerformMoveLocked(repository, content, parentId, parent, userId, moves, trashed); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } var moveInfo = moves //changes @@ -1649,6 +1603,7 @@ namespace Umbraco.Core.Services EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this); uow.Complete(); + TreeChanged.RaiseEvent(deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs(), this); } Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); @@ -1707,7 +1662,6 @@ namespace Umbraco.Core.Services // save repository.AddOrUpdate(copy); - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Flush(); // ensure copy has an ID - fixme why? @@ -1729,7 +1683,6 @@ namespace Umbraco.Core.Services dcopy.WriterId = userId; repository.AddOrUpdate(dcopy); - repository.AddOrUpdatePreviewXml(dcopy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); copyIds[descendant.Id] = dcopy; } @@ -1743,6 +1696,7 @@ namespace Umbraco.Core.Services uow.Complete(); } + TreeChanged.RaiseEvent(new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); return copy; @@ -1805,11 +1759,12 @@ namespace Umbraco.Core.Services content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs(), this); + Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); return content; @@ -1868,12 +1823,8 @@ namespace Umbraco.Core.Services // save saved.Add(content); repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } - foreach (var content in published) - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - uow.Complete(); } @@ -1883,6 +1834,8 @@ namespace Umbraco.Core.Services if (raiseEvents && published.Any()) Published.RaiseEvent(new PublishEventArgs(published, false, false), this); + TreeChanged.RaiseEvent(saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs(), this); + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); return true; @@ -1992,8 +1945,6 @@ namespace Umbraco.Core.Services var publishedItem = status.ContentItem; publishedItem.WriterId = userId; repository.AddOrUpdate(publishedItem); - repository.AddOrUpdatePreviewXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - repository.AddOrUpdateContentXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); publishedItems.Add(publishedItem); } @@ -2001,6 +1952,7 @@ namespace Umbraco.Core.Services } Published.RaiseEvent(new PublishEventArgs(publishedItems, false, false), this); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); return attempts; } @@ -2041,13 +1993,13 @@ namespace Umbraco.Core.Services content.WriterId = userId; repository.AddOrUpdate(content); - // fixme delete xml from database! was in _publishingStrategy.UnPublishingFinalized(content); - repository.DeleteContentXml(content); uow.Complete(); } UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content)); } @@ -2066,8 +2018,9 @@ namespace Umbraco.Core.Services return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); var isNew = content.IsNewEntity(); + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; var previouslyPublished = content.HasIdentity && content.HasPublishedVersion; - var status = default(Attempt); + Attempt status; using (var uow = UowProvider.CreateUnitOfWork()) { @@ -2094,9 +2047,6 @@ namespace Umbraco.Core.Services content.WriterId = userId; repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - if (content.Published) - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } @@ -2104,6 +2054,7 @@ namespace Umbraco.Core.Services if (status.Success == false) { // fixme what about the saved event? + TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); return status; } @@ -2121,6 +2072,9 @@ namespace Umbraco.Core.Services } } + // invalidate the node/branch + TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); return status; } @@ -2298,6 +2252,11 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> UnPublished; + /// + /// Occurs after change. + /// + internal static event TypedEventHandler.EventArgs> TreeChanged; + #endregion #region Publishing Strategies @@ -2524,6 +2483,7 @@ namespace Umbraco.Core.Services // The main problem with this is that for every content item being deleted, events are raised... // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + var changes = new List>(); var moves = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) @@ -2556,11 +2516,13 @@ namespace Umbraco.Core.Services { // see MoveToRecycleBin PerformMoveLocked(repository, child, Constants.System.RecycleBinContent, null, userId, moves, true); + changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch)); } // delete content // triggers the deleted event (and handles the files) DeleteLocked(repository, content); + changes.Add(new TreeChange(content, TreeChangeTypes.Remove)); } uow.Complete(); @@ -2571,6 +2533,7 @@ namespace Umbraco.Core.Services .ToArray(); if (moveInfos.Length > 0) Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this); + TreeChanged.RaiseEvent(changes.ToEventArgs(), this); Audit(AuditType.Delete, $"Delete Content of Type {contentTypeId} performed by user", userId, Constants.System.Root); } @@ -2596,66 +2559,5 @@ namespace Umbraco.Core.Services } #endregion - - #region Xml - Should Move! - - /// - /// Returns the persisted content's XML structure - /// - /// - /// - public XElement GetContentXml(int contentId) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(Constants.Locks.ContentTree); - var repository = uow.CreateRepository(); - var elt = repository.GetContentXml(contentId); - uow.Complete(); - return elt; - } - } - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - public XElement GetContentPreviewXml(int contentId, Guid version) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(Constants.Locks.ContentTree); - var repository = uow.CreateRepository(); - var elt = repository.GetContentPreviewXml(contentId, version); - uow.Complete(); - return elt; - } - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(Constants.Locks.ContentTree); - var repository = uow.CreateRepository(); - repository.RebuildXmlStructures( - content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - uow.Complete(); - } - - Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index fcbad641cf..1c0d38410f 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -25,6 +21,7 @@ namespace Umbraco.Core.Services _contentService = contentService; } + protected override IContentTypeService Instance => this; // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes }; @@ -88,69 +85,5 @@ namespace Umbraco.Core.Services return aliases; } } - - /// - /// Generates the complete (simplified) XML DTD. - /// - /// The DTD as a string - public string GetDtd() - { - var dtd = new StringBuilder(); - dtd.AppendLine(""); - - return dtd.ToString(); - } - - /// - /// Generates the complete XML DTD without the root. - /// - /// The DTD as a string - public string GetContentTypesDtd() - { - var dtd = new StringBuilder(); - try - { - var strictSchemaBuilder = new StringBuilder(); - - var contentTypes = GetAll(new int[0]); - foreach (ContentType contentType in contentTypes) - { - string safeAlias = contentType.Alias.ToSafeAlias(); - if (safeAlias != null) - { - strictSchemaBuilder.AppendLine($""); - strictSchemaBuilder.AppendLine($""); - } - } - - // Only commit the strong schema to the container if we didn't generate an error building it - dtd.Append(strictSchemaBuilder); - } - catch (Exception exception) - { - LogHelper.Error("Error while trying to build DTD for Xml schema; is Umbraco installed correctly and the connection string configured?", exception); - } - return dtd.ToString(); - } - - protected override void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) - { - var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); - if (toUpdate.Any() == false) return; - - var contentService = _contentService as ContentService; - if (contentService != null) - { - contentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); - } - else - { - //this should never occur, the content service should always be typed but we'll check anyways. - _contentService.RePublishAll(); - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index 04216f558a..3ed56b5217 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services.Changes; namespace Umbraco.Core.Services { @@ -17,87 +18,6 @@ namespace Umbraco.Core.Services protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) { } - - /// - /// This is called after an content type is saved and is used to update the content xml structures in the database - /// if they are required to be updated. - /// - /// - internal IEnumerable GetContentTypesForXmlUpdates(params IContentTypeBase[] contentTypes) - { - - var toUpdate = new List(); - - foreach (var contentType in contentTypes) - { - //we need to determine if we need to refresh the xml content in the database. This is to be done when: - // - the item is not new (already existed in the db) AND - // - a content type changes it's alias OR - // - if a content type has it's property removed OR - // - if a content type has a property whose alias has changed - //here we need to check if the alias of the content type changed or if one of the properties was removed. - var dirty = contentType as IRememberBeingDirty; - if (dirty == null) continue; - - //check if any property types have changed their aliases (and not new property types) - var hasAnyPropertiesChangedAlias = contentType.PropertyTypes.Any(propType => - { - var dirtyProperty = propType as IRememberBeingDirty; - if (dirtyProperty == null) return false; - return dirtyProperty.WasPropertyDirty("HasIdentity") == false //ensure it's not 'new' - && dirtyProperty.WasPropertyDirty("Alias"); //alias has changed - }); - - if (dirty.WasPropertyDirty("HasIdentity") == false //ensure it's not 'new' - && (dirty.WasPropertyDirty("Alias") || dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") || hasAnyPropertiesChangedAlias)) - { - //If the alias was changed then we only need to update the xml structures for content of the current content type. - //If a property was deleted or a property alias was changed then we need to update the xml structures for any - // content of the current content type and any of the content type's child content types. - if (dirty.WasPropertyDirty("Alias") - && dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") == false && hasAnyPropertiesChangedAlias == false) - { - //if only the alias changed then only update the current content type - toUpdate.Add(contentType); - } - else - { - //TODO: This is pretty nasty, fix this - var contentTypeService = this as IContentTypeService; - if (contentTypeService != null) - { - //if a property was deleted or alias changed, then update all content of the current content type - // and all of it's desscendant doc types. - toUpdate.AddRange(((IContentType) contentType).DescendantsAndSelf(contentTypeService)); - } - else - { - var mediaTypeService = this as IMediaTypeService; - if (mediaTypeService != null) - { - //if a property was deleted or alias changed, then update all content of the current content type - // and all of it's desscendant doc types. - toUpdate.AddRange(((IMediaType) contentType).DescendantsAndSelf(mediaTypeService)); - } - else - { - var memberTypeService = this as IMemberTypeService; - if (memberTypeService != null) - { - //if a property was deleted or alias changed, then update all content of the current content type - // and all of it's desscendant doc types. - toUpdate.AddRange(((IMemberType)contentType).DescendantsAndSelf(memberTypeService)); - } - } - } - - } - } - } - - return toUpdate; - - } } internal abstract class ContentTypeServiceBase : ContentTypeServiceBase @@ -106,29 +26,40 @@ namespace Umbraco.Core.Services { protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) + { } + + protected abstract TService Instance { get; } + + internal static event TypedEventHandler.EventArgs> Changed; + + protected void OnChanged(ContentTypeChange.EventArgs args) { - _this = this as TService; - if (_this == null) throw new Exception("Oops."); + Changed.RaiseEvent(args, Instance); } - private readonly TService _this; + public static event TypedEventHandler.EventArgs> UowRefreshedEntity; + + protected void OnUowRefreshedEntity(ContentTypeChange.EventArgs args) + { + UowRefreshedEntity.RaiseEvent(args, Instance); + } public static event TypedEventHandler> Saving; public static event TypedEventHandler> Saved; protected void OnSaving(SaveEventArgs args) { - Saving.RaiseEvent(args, _this); + Saving.RaiseEvent(args, Instance); } protected bool OnSavingCancelled(SaveEventArgs args) { - return Saving.IsRaisedEventCancelled(args, _this); + return Saving.IsRaisedEventCancelled(args, Instance); } protected void OnSaved(SaveEventArgs args) { - Saved.RaiseEvent(args, _this); + Saved.RaiseEvent(args, Instance); } public static event TypedEventHandler> Deleting; @@ -136,12 +67,12 @@ namespace Umbraco.Core.Services protected void OnDeleting(DeleteEventArgs args) { - Deleting.RaiseEvent(args, _this); + Deleting.RaiseEvent(args, Instance); } protected bool OnDeletingCancelled(DeleteEventArgs args) { - return Deleting.IsRaisedEventCancelled(args, _this); + return Deleting.IsRaisedEventCancelled(args, Instance); } protected void OnDeleted(DeleteEventArgs args) @@ -154,17 +85,17 @@ namespace Umbraco.Core.Services protected void OnMoving(MoveEventArgs args) { - Moving.RaiseEvent(args, _this); + Moving.RaiseEvent(args, Instance); } protected bool OnMovingCancelled(MoveEventArgs args) { - return Moving.IsRaisedEventCancelled(args, _this); + return Moving.IsRaisedEventCancelled(args, Instance); } protected void OnMoved(MoveEventArgs args) { - Moved.RaiseEvent(args, _this); + Moved.RaiseEvent(args, Instance); } public static event TypedEventHandler> SavingContainer; @@ -172,17 +103,17 @@ namespace Umbraco.Core.Services protected void OnSavingContainer(SaveEventArgs args) { - SavingContainer.RaiseEvent(args, _this); + SavingContainer.RaiseEvent(args, Instance); } protected bool OnSavingContainerCancelled(SaveEventArgs args) { - return SavingContainer.IsRaisedEventCancelled(args, _this); + return SavingContainer.IsRaisedEventCancelled(args, Instance); } protected void OnSavedContainer(SaveEventArgs args) { - SavedContainer.RaiseEvent(args, _this); + SavedContainer.RaiseEvent(args, Instance); } public static event TypedEventHandler> DeletingContainer; @@ -190,26 +121,18 @@ namespace Umbraco.Core.Services protected void OnDeletingContainer(DeleteEventArgs args) { - DeletingContainer.RaiseEvent(args, _this); + DeletingContainer.RaiseEvent(args, Instance); } protected bool OnDeletingContainerCancelled(DeleteEventArgs args) { - return DeletingContainer.IsRaisedEventCancelled(args, _this); + return DeletingContainer.IsRaisedEventCancelled(args, Instance); } protected void OnDeletedContainer(DeleteEventArgs args) { - DeletedContainer.RaiseEvent(args, _this); + DeletedContainer.RaiseEvent(args, Instance); } - - // for later usage - //public static event TypedEventHandler TxRefreshed; - - //protected void OnTxRefreshed(Change.EventArgs args) - //{ - // TxRefreshed.RaiseEvent(args, this); - //} } internal abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeServiceBase @@ -297,6 +220,92 @@ namespace Umbraco.Core.Services #endregion #region Composition + + internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes) + { + // find all content types impacted by the changes, + // - content type alias changed + // - content type property removed, or alias changed + // - content type composition removed (not testing if composition had properties...) + // + // because these are the changes that would impact the raw content data + + // note + // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere + // instead of IsPropertyDirty() since dirty properties have been resetted already + + var changes = new List>(); + + foreach (var contentType in contentTypes) + { + var dirty = (IRememberBeingDirty)contentType; + + // skip new content types + var isNewContentType = dirty.WasPropertyDirty("HasIdentity"); + if (isNewContentType) + { + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); + continue; + } + + // alias change? + var hasAliasChanged = dirty.WasPropertyDirty("Alias"); + + // existing property alias change? + var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType => + { + var dirtyProperty = propertyType as IRememberBeingDirty; + if (dirtyProperty == null) throw new Exception("oops"); + + // skip new properties + var isNewProperty = dirtyProperty.WasPropertyDirty("HasIdentity"); + if (isNewProperty) return false; + + // alias change? + var hasPropertyAliasBeenChanged = dirtyProperty.WasPropertyDirty("Alias"); + return hasPropertyAliasBeenChanged; + }); + + // removed properties? + var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved"); + + // removed compositions? + var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved"); + + // main impact on properties? + var hasPropertyMainImpact = hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias; + + if (hasAliasChanged || hasPropertyMainImpact) + { + // add that one, as a main change + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain); + + if (hasPropertyMainImpact) + foreach (var c in contentType.ComposedOf(this)) + AddChange(changes, c, ContentTypeChangeTypes.RefreshMain); + } + else + { + // add that one, as an other change + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); + } + } + + return changes; + } + + // ensures changes contains no duplicates + private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes) + { + var change = changes.FirstOrDefault(x => x.Item == contentType); + if (change == null) + { + changes.Add(new ContentTypeChange(contentType, changeTypes)); + return; + } + change.ChangeTypes |= changeTypes; + } + #endregion #region Get, Has, Is, Count @@ -514,15 +523,15 @@ namespace Umbraco.Core.Services item.CreatorId = userId; repo.AddOrUpdate(item); // also updates content/media/member items uow.Flush(); // to db but no commit yet - - // ... - + + // figure out impacted content types + var changes = ComposeContentTypeChanges(item).ToArray(); + var args = changes.ToEventArgs(); + OnUowRefreshedEntity(args); uow.Complete(); + OnChanged(args); } - // todo: should use TxRefreshed event within the transaction instead, see CC branch - UpdateContentXmlStructure(item); - OnSaved(new SaveEventArgs(item, false)); Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); } @@ -552,10 +561,13 @@ namespace Umbraco.Core.Services //save it all in one go uow.Complete(); - } - // todo: should use TxRefreshed event within the transaction instead, see CC branch - UpdateContentXmlStructure(itemsA.Cast().ToArray()); + // figure out impacted content types + var changes = ComposeContentTypeChanges(itemsA).ToArray(); + var args = changes.ToEventArgs(); + OnUowRefreshedEntity(args); + OnChanged(args); + } OnSaved(new SaveEventArgs(itemsA, false)); Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); @@ -579,6 +591,14 @@ namespace Umbraco.Core.Services var descendantsAndSelf = item.DescendantsAndSelf(this) .ToArray(); + // all impacted (through composition) probably lose some properties + // don't try to be too clever here, just report them all + // do this before anything is deleted + var changed = descendantsAndSelf.SelectMany(xx => xx.ComposedOf(this)) + .Distinct() + .Except(descendantsAndSelf) + .ToArray(); + // delete content DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); @@ -592,8 +612,13 @@ namespace Umbraco.Core.Services uow.Flush(); // to db but no commit yet //... + var changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) + .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); + var args = changes.ToEventArgs(); + OnUowRefreshedEntity(args); uow.Complete(); + OnChanged(args); } OnDeleted(new DeleteEventArgs(item, false)); @@ -617,6 +642,14 @@ namespace Umbraco.Core.Services .Distinct() .ToArray(); + // all impacted (through composition) probably lose some properties + // don't try to be too clever here, just report them all + // do this before anything is deleted + var changed = allDescendantsAndSelf.SelectMany(x => x.ComposedOf(this)) + .Distinct() + .Except(allDescendantsAndSelf) + .ToArray(); + // delete content DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id)); @@ -627,10 +660,15 @@ namespace Umbraco.Core.Services uow.Flush(); // to db but no commit yet - // ... - + var changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) + .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); + var args = changes.ToEventArgs(); + uow.Complete(); + + OnUowRefreshedEntity(args); + OnChanged(args); } OnDeleted(new DeleteEventArgs(itemsA, false)); @@ -785,6 +823,10 @@ namespace Umbraco.Core.Services } } + + // FIXME should raise Changed events for the content type that is MOVED and ALL ITS CHILDREN IF ANY + // FIXME at the moment we don't have no MoveContainer don't we?! + OnMoved(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); @@ -975,11 +1017,5 @@ namespace Umbraco.Core.Services } #endregion - - #region Xml - Should Move! - - protected abstract void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes); - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index a5a3f35683..e8d704e686 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -29,10 +29,10 @@ namespace Umbraco.Core.Services /// /// Saves a collection of objects. - /// + /// /// Collection of to save /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. + /// Optional boolean indicating whether or not to raise events. Attempt Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); /// @@ -99,32 +99,6 @@ namespace Umbraco.Core.Services /// public interface IContentService : IService { - - - /// - /// Returns the persisted content's XML structure - /// - /// - /// - XElement GetContentXml(int contentId); - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - XElement GetContentPreviewXml(int contentId, Guid version); - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); - int CountPublished(string contentTypeAlias = null); int Count(string contentTypeAlias = null); int CountChildren(int parentId, string contentTypeAlias = null); @@ -222,7 +196,7 @@ namespace Umbraco.Core.Services /// Id of the Parent to retrieve Children from /// An Enumerable list of objects IEnumerable GetChildren(int id); - + /// /// Gets a collection of objects by Parent Id /// @@ -251,7 +225,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); - + /// /// Gets a collection of objects by Parent Id /// @@ -323,7 +297,7 @@ namespace Umbraco.Core.Services /// /// Saves a collection of objects. - /// + /// /// Collection of to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. @@ -442,13 +416,6 @@ namespace Umbraco.Core.Services /// True if the content has any published version otherwise False bool HasPublishedVersion(int id); - /// - /// Re-Publishes all Content - /// - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool RePublishAll(int userId = 0); - /// /// Publishes a single object /// @@ -520,11 +487,11 @@ namespace Umbraco.Core.Services /// /// Please note that this method will completely remove the Content from the database /// The to delete - /// Optional Id of the User deleting the Content + /// Optional Id of the User deleting the Content void Delete(IContent content, int userId = 0); /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// Copies an object by creating a new Content object of the same type and copies all data from the current /// to the new copy, which is returned. Recursively copies all children. /// /// The to copy @@ -535,7 +502,7 @@ namespace Umbraco.Core.Services IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// Copies an object by creating a new Content object of the same type and copies all data from the current /// to the new copy which is returned. /// /// The to copy diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 14f48bb156..e0d4b3549e 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -24,17 +24,5 @@ namespace Umbraco.Core.Services /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); - - /// - /// Generates the complete (simplified) XML DTD. - /// - /// The DTD as a string - string GetDtd(); - - /// - /// Generates the complete XML DTD without the root. - /// - /// The DTD as a string - string GetContentTypesDtd(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index b231119561..e06cf815c1 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -58,15 +58,6 @@ namespace Umbraco.Core.Services /// public interface IMediaService : IService { - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - void RebuildXmlStructures(params int[] contentTypeIds); - int Count(string mediaTypeAlias = null); int CountChildren(int parentId, string mediaTypeAlias = null); int CountDescendants(int parentId, string mediaTypeAlias = null); diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 6591cc0b74..96029c6a4d 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -13,15 +13,6 @@ namespace Umbraco.Core.Services /// public interface IMemberService : IMembershipMemberService { - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); - /// /// Gets a list of paged objects /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 79f52d7b5c..8792c35852 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -11,7 +10,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Strings; +using Umbraco.Core.Services.Changes; namespace Umbraco.Core.Services { @@ -20,10 +19,6 @@ namespace Umbraco.Core.Services /// public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations { - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - private readonly IEnumerable _urlSegmentProviders; private IMediaTypeService _mediaTypeService; #region Constructors @@ -31,19 +26,9 @@ namespace Umbraco.Core.Services public MediaService( IDatabaseUnitOfWorkProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IDataTypeService dataTypeService, - IUserService userService, - IEnumerable urlSegmentProviders) + IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) - { - if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); - if (userService == null) throw new ArgumentNullException(nameof(userService)); - if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); - _dataTypeService = dataTypeService; - _userService = userService; - _urlSegmentProviders = urlSegmentProviders; - } + { } // don't change or remove this, will need it later private IMediaTypeService MediaTypeService => _mediaTypeService; @@ -275,10 +260,9 @@ namespace Umbraco.Core.Services var repo = uow.CreateRepository(); repo.AddOrUpdate(media); - // FIXME contentXml?! - repo.AddOrUpdatePreviewXml(media, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); Saved.RaiseEvent(new SaveEventArgs(media, false), this); + TreeChanged.RaiseEvent(new TreeChange(media, TreeChangeTypes.RefreshNode).ToEventArgs(), this); } Created.RaiseEvent(new NewEventArgs(media, false, media.ContentType.Alias, parent), this); @@ -566,7 +550,7 @@ namespace Umbraco.Core.Services /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field - /// + /// /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) { @@ -753,6 +737,8 @@ namespace Umbraco.Core.Services if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(media, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); + var isNew = media.IsNewEntity(); + using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.MediaTree); @@ -761,17 +747,13 @@ namespace Umbraco.Core.Services if (media.HasIdentity == false) media.CreatorId = userId; repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); - - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); - uow.Complete(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(media, false, evtMsgs), this); + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + TreeChanged.RaiseEvent(new TreeChange(media, changeType).ToEventArgs(), this); Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); return OperationStatus.Attempt.Succeed(evtMsgs); @@ -802,6 +784,9 @@ namespace Umbraco.Core.Services if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(mediasA, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); + var treeChanges = mediasA.Select(x => new TreeChange(x, + x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.MediaTree); @@ -811,11 +796,6 @@ namespace Umbraco.Core.Services if (media.HasIdentity == false) media.CreatorId = userId; repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); - - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); } uow.Complete(); @@ -823,6 +803,7 @@ namespace Umbraco.Core.Services if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(mediasA, false, evtMsgs), this); + TreeChanged.RaiseEvent(treeChanges.ToEventArgs(), this); Audit(AuditType.Save, "Bulk Save media performed by user", userId == -1 ? 0 : userId, Constants.System.Root); return OperationStatus.Attempt.Succeed(evtMsgs); @@ -870,6 +851,7 @@ namespace Umbraco.Core.Services DeleteLocked(repository, media); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(media, TreeChangeTypes.Remove).ToEventArgs(), this); } Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); @@ -1004,6 +986,7 @@ namespace Umbraco.Core.Services PerformMoveLocked(repository, media, Constants.System.RecycleBinMedia, null, userId, moves, true); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } var moveInfo = moves @@ -1053,6 +1036,7 @@ namespace Umbraco.Core.Services PerformMoveLocked(repository, media, parentId, parent, userId, moves, trashed); uow.Complete(); + TreeChanged.RaiseEvent(new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } var moveInfo = moves //changes @@ -1145,6 +1129,7 @@ namespace Umbraco.Core.Services EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this); uow.Complete(); + TreeChanged.RaiseEvent(deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs(), this); } Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, Constants.System.RecycleBinMedia); @@ -1193,11 +1178,6 @@ namespace Umbraco.Core.Services // save saved.Add(media); repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); - - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, m)); } uow.Complete(); @@ -1205,6 +1185,7 @@ namespace Umbraco.Core.Services if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(saved, false), this); + TreeChanged.RaiseEvent(saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs(), this); Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); return true; @@ -1303,6 +1284,11 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler EmptiedRecycleBin; + /// + /// Occurs after change. + /// + internal static event TypedEventHandler.EventArgs> TreeChanged; + #endregion #region Content Types @@ -1322,6 +1308,7 @@ namespace Umbraco.Core.Services // The main problem with this is that for every content item being deleted, events are raised... // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + var changes = new List>(); var moves = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) @@ -1348,11 +1335,13 @@ namespace Umbraco.Core.Services { // see MoveToRecycleBin PerformMoveLocked(repository, child, Constants.System.RecycleBinMedia, null, userId, moves, true); + changes.Add(new TreeChange(media, TreeChangeTypes.RefreshBranch)); } // delete media // triggers the deleted event (and handles the files) DeleteLocked(repository, media); + changes.Add(new TreeChange(media, TreeChangeTypes.Remove)); } uow.Complete(); @@ -1363,6 +1352,7 @@ namespace Umbraco.Core.Services .ToArray(); if (moveInfos.Length > 0) Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this); + TreeChanged.RaiseEvent(changes.ToEventArgs(), this); Audit(AuditType.Delete, $"Delete Media of Type {mediaTypeId} performed by user", userId, Constants.System.Root); } @@ -1388,31 +1378,5 @@ namespace Umbraco.Core.Services } #endregion - - #region Xml - Should Move! - - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(Constants.Locks.MediaTree); - var repository = uow.CreateRepository(); - repository.RebuildXmlStructures( - media => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, media), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - uow.Complete(); - } - - Audit(AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - #endregion } } diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs index 69f26c52f2..0eeccb4f5b 100644 --- a/src/Umbraco.Core/Services/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -19,6 +18,8 @@ namespace Umbraco.Core.Services _mediaService = mediaService; } + protected override IMediaTypeService Instance => this; + // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.MediaTypes }; protected override int[] WriteLockIds { get; } = { Constants.Locks.MediaTree, Constants.Locks.MediaTypes }; @@ -44,14 +45,5 @@ namespace Umbraco.Core.Services foreach (var typeId in typeIds) MediaService.DeleteMediaOfType(typeId); } - - protected override void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) - { - var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); - if (toUpdate.Any() == false) return; - - var mediaService = _mediaService as MediaService; - mediaService?.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } } } diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 46aea609da..6b0ba8f33f 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -21,8 +21,6 @@ namespace Umbraco.Core.Services /// public class MemberService : RepositoryService, IMemberService { - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; private readonly IMemberGroupService _memberGroupService; private IMemberTypeService _memberTypeService; @@ -32,14 +30,11 @@ namespace Umbraco.Core.Services IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IMemberGroupService memberGroupService, - IDataTypeService dataTypeService) + IMemberGroupService memberGroupService) : base(provider, logger, eventMessagesFactory) { if (memberGroupService == null) throw new ArgumentNullException(nameof(memberGroupService)); - if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); _memberGroupService = memberGroupService; - _dataTypeService = dataTypeService; } // don't change or remove this, will need it later @@ -316,12 +311,6 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.AddOrUpdate(member); - // fixme kill - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - Saved.RaiseEvent(new SaveEventArgs(member, false), this); } @@ -909,13 +898,6 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(member); - // fixme get rid of xml - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - uow.Complete(); } @@ -942,17 +924,8 @@ namespace Umbraco.Core.Services uow.WriteLock(Constants.Locks.MemberTree); var repository = uow.CreateRepository(); foreach (var member in membersA) - { repository.AddOrUpdate(member); - // fixme get rid of xml stuff - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - uow.Complete(); } @@ -1369,31 +1342,5 @@ namespace Umbraco.Core.Services } #endregion - - #region Xml - Should Move! - - /// - /// Rebuilds all xml content in the cmsContentXml table for all members - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all members = USE WITH CARE! - /// - /// True if publishing succeeded, otherwise False - public void RebuildXmlStructures(params int[] memberTypeIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.RebuildXmlStructures( - member => _entitySerializer.Serialize(_dataTypeService, member), - contentTypeIds: memberTypeIds.Length == 0 ? null : memberTypeIds); - uow.Complete(); - } - - Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index df78b59bd1..2fbe8b1784 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -19,6 +18,8 @@ namespace Umbraco.Core.Services _memberService = memberService; } + protected override IMemberTypeService Instance => this; + // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.MemberTypes }; protected override int[] WriteLockIds { get; } = { Constants.Locks.MemberTree, Constants.Locks.MemberTypes }; @@ -45,16 +46,6 @@ namespace Umbraco.Core.Services MemberService.DeleteMembersOfType(typeId); } - protected override void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) - { - - var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); - if (toUpdate.Any() == false) return; - - var memberService = _memberService as MemberService; - memberService?.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } - public string GetDefault() { using (var uow = UowProvider.CreateUnitOfWork()) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 3383d97eb1..94b2cb1981 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -101,8 +101,8 @@ namespace Umbraco.Core //if the resolution was success, return it, otherwise just return the path, we've detected // it's a path but maybe it's relative and resolution has failed, etc... in which case we're just // returning what was given to us. - return resolvedUrlResult.Success - ? resolvedUrlResult + return resolvedUrlResult.Success + ? resolvedUrlResult : Attempt.Succeed(input); } } @@ -111,7 +111,7 @@ namespace Umbraco.Core } /// - /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing + /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing /// a try/catch when deserializing when it is not json. /// /// @@ -221,7 +221,7 @@ namespace Umbraco.Core /// /// /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are - /// delimited properly with '&' + /// delimited properly with '&' /// internal static string AppendQueryStringToUrl(this string url, params string[] queryStrings) { @@ -667,6 +667,16 @@ namespace Umbraco.Core return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); } + public static int InvariantIndexOf(this string s, string value) + { + return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); + } + + public static int InvariantLastIndexOf(this string s, string value) + { + return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); + } + /// /// Determines if the string is a Guid /// @@ -1038,7 +1048,7 @@ namespace Umbraco.Core } // FORMAT STRINGS - + /// /// Cleans a string to produce a string that can safely be used in an alias. /// @@ -1125,7 +1135,7 @@ namespace Umbraco.Core /// Cleans a string. /// /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, + /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The clean string. /// The string is cleaned in the context of the IShortStringHelper default culture. @@ -1138,7 +1148,7 @@ namespace Umbraco.Core /// Cleans a string, using a specified separator. /// /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, + /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The separator. /// The clean string. @@ -1152,7 +1162,7 @@ namespace Umbraco.Core /// Cleans a string in the context of a specified culture. /// /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, + /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The culture. /// The clean string. @@ -1165,7 +1175,7 @@ namespace Umbraco.Core /// Cleans a string in the context of a specified culture, using a specified separator. /// /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, + /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The separator. /// The culture. @@ -1222,7 +1232,7 @@ namespace Umbraco.Core } /// - /// An extension method that returns a new string in which all occurrences of a + /// An extension method that returns a new string in which all occurrences of a /// specified string in the current instance are replaced with another specified string. /// StringComparison specifies the type of search to use for the specified string. /// @@ -1235,7 +1245,7 @@ namespace Umbraco.Core { // This initialisation ensures the first check starts at index zero of the source. On successive checks for // a match, the source is skipped to immediately after the last replaced occurrence for efficiency - // and to avoid infinite loops when oldString and newString compare equal. + // and to avoid infinite loops when oldString and newString compare equal. int index = -1 * newString.Length; // Determine if there are any matches left in source, starting from just after the result of replacing the last match. @@ -1344,12 +1354,12 @@ namespace Umbraco.Core /// - /// An extension method that returns a new string in which all occurrences of an - /// unicode characters that are invalid in XML files are replaced with an empty string. + /// An extension method that returns a new string in which all occurrences of an + /// unicode characters that are invalid in XML files are replaced with an empty string. /// /// Current instance of the string /// Updated string - /// + /// /// /// removes any unusual unicode characters that can't be encoded into XML /// diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index c16ce5a1a8..dc77314cee 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -442,7 +442,7 @@ namespace Umbraco.Core.Sync { var jsonRefresher = refresher as IJsonCacheRefresher; if (jsonRefresher == null) - throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.RefresherUniqueId + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); return jsonRefresher; } diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs index 6222ef4002..1eb2ea0f67 100644 --- a/src/Umbraco.Core/Sync/IServerMessenger.cs +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Sync /// The servers that compose the load balanced environment. /// The ICacheRefresher. /// The notification content. - void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, object payload); + void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, TPayload[] payload); /// /// Notifies the distributed cache, for a specified . diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index 2b5b1cafe0..8c1cfca96d 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Sync private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) { - RefresherId = refresher.UniqueIdentifier; + RefresherId = refresher.RefresherUniqueId; RefreshType = refreshType; } diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs index d22d292e76..5b15dce392 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -55,29 +55,29 @@ namespace Umbraco.Core.Sync #region IServerMessenger - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, object payload) + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, TPayload[] payload) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (payload == null) throw new ArgumentNullException("payload"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); + if (payload == null) throw new ArgumentNullException(nameof(payload)); Deliver(servers, refresher, payload); } public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (jsonPayload == null) throw new ArgumentNullException("jsonPayload"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); + if (jsonPayload == null) throw new ArgumentNullException(nameof(jsonPayload)); Deliver(servers, refresher, MessageType.RefreshByJson, json: jsonPayload); } public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); + if (getNumericId == null) throw new ArgumentNullException(nameof(getNumericId)); if (instances == null || instances.Length == 0) return; Func getId = x => getNumericId(x); @@ -86,9 +86,9 @@ namespace Umbraco.Core.Sync public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (getGuidId == null) throw new ArgumentNullException("getGuidId"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); + if (getGuidId == null) throw new ArgumentNullException(nameof(getGuidId)); if (instances == null || instances.Length == 0) return; Func getId = x => getGuidId(x); @@ -97,9 +97,9 @@ namespace Umbraco.Core.Sync public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); + if (getNumericId == null) throw new ArgumentNullException(nameof(getNumericId)); if (instances == null || instances.Length == 0) return; Func getId = x => getNumericId(x); @@ -108,8 +108,8 @@ namespace Umbraco.Core.Sync public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); if (numericIds == null || numericIds.Length == 0) return; Deliver(servers, refresher, MessageType.RemoveById, numericIds.Cast()); @@ -117,8 +117,8 @@ namespace Umbraco.Core.Sync public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); if (numericIds == null || numericIds.Length == 0) return; Deliver(servers, refresher, MessageType.RefreshById, numericIds.Cast()); @@ -126,8 +126,8 @@ namespace Umbraco.Core.Sync public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); if (guidIds == null || guidIds.Length == 0) return; Deliver(servers, refresher, MessageType.RefreshById, guidIds.Cast()); @@ -135,8 +135,8 @@ namespace Umbraco.Core.Sync public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); Deliver(servers, refresher, MessageType.RefreshAll); } @@ -153,16 +153,16 @@ namespace Umbraco.Core.Sync #region Deliver - protected void DeliverLocal(ICacheRefresher refresher, object payload) + protected void DeliverLocal(ICacheRefresher refresher, TPayload[] payload) { - if (refresher == null) throw new ArgumentNullException("refresher"); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); LogHelper.Debug("Invoking refresher {0} on local server for message type RefreshByPayload", refresher.GetType); - var payloadRefresher = refresher as IPayloadCacheRefresher; + var payloadRefresher = refresher as IPayloadCacheRefresher; if (payloadRefresher == null) - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IPayloadCacheRefresher)); + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IPayloadCacheRefresher)); payloadRefresher.Refresh(payload); } @@ -178,7 +178,7 @@ namespace Umbraco.Core.Sync /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) { - if (refresher == null) throw new ArgumentNullException("refresher"); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", refresher.GetType, @@ -241,7 +241,7 @@ namespace Umbraco.Core.Sync /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) { - if (refresher == null) throw new ArgumentNullException("refresher"); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", refresher.GetType, @@ -291,10 +291,10 @@ namespace Umbraco.Core.Sync //protected abstract void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, object payload); - protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, object payload) + protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, TPayload[] payload) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); var serversA = servers.ToArray(); @@ -312,11 +312,11 @@ namespace Umbraco.Core.Sync protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); var serversA = servers.ToArray(); - var idsA = ids == null ? null : ids.ToArray(); + var idsA = ids?.ToArray(); // deliver local DeliverLocal(refresher, messageType, idsA, json); @@ -331,8 +331,8 @@ namespace Umbraco.Core.Sync protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); + if (servers == null) throw new ArgumentNullException(nameof(servers)); + if (refresher == null) throw new ArgumentNullException(nameof(refresher)); var serversA = servers.ToArray(); var instancesA = instances.ToArray(); diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs index 5a9af4e83d..221f32e31b 100644 --- a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs +++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs @@ -191,11 +191,11 @@ namespace Umbraco.Core.Sync switch (messageType) { case MessageType.RefreshByJson: - asyncResults.Add(client.BeginRefreshByJson(refresher.UniqueIdentifier, jsonPayload, Login, Password, null, null)); + asyncResults.Add(client.BeginRefreshByJson(refresher.RefresherUniqueId, jsonPayload, Login, Password, null, null)); break; case MessageType.RefreshAll: - asyncResults.Add(client.BeginRefreshAll(refresher.UniqueIdentifier, Login, Password, null, null)); + asyncResults.Add(client.BeginRefreshAll(refresher.RefresherUniqueId, Login, Password, null, null)); break; case MessageType.RefreshById: @@ -206,14 +206,14 @@ namespace Umbraco.Core.Sync { // bulk of ints is supported var json = JsonConvert.SerializeObject(ids.Cast().ToArray()); - var result = client.BeginRefreshByIds(refresher.UniqueIdentifier, json, Login, Password, null, null); + var result = client.BeginRefreshByIds(refresher.RefresherUniqueId, json, Login, Password, null, null); asyncResults.Add(result); } else // must be guids { // bulk of guids is not supported, iterate asyncResults.AddRange(ids.Select(i => - client.BeginRefreshByGuid(refresher.UniqueIdentifier, (Guid)i, Login, Password, null, null))); + client.BeginRefreshByGuid(refresher.RefresherUniqueId, (Guid)i, Login, Password, null, null))); } break; @@ -223,7 +223,7 @@ namespace Umbraco.Core.Sync // must be ints asyncResults.AddRange(ids.Select(i => - client.BeginRemoveById(refresher.UniqueIdentifier, (int)i, Login, Password, null, null))); + client.BeginRemoveById(refresher.RefresherUniqueId, (int)i, Login, Password, null, null))); break; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d0e50977fa..5212bab02a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -380,6 +380,7 @@ Files.resx + @@ -545,6 +546,12 @@ + + + + + + @@ -562,7 +569,6 @@ - diff --git a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs index aebfb906f7..b44b7d5f6e 100644 --- a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs @@ -971,6 +971,10 @@ namespace Umbraco.Core.Xml.XPath ParameterNavigator }; + // gets the state + // for unit tests only + internal State InternalState { get { return _state; } } + // represents the XPathNavigator state internal class State { diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs index c65e655d38..565f69fef1 100644 --- a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -1,4 +1,18 @@ -using System; +// DEBUG + +// We make sure that diagnostics code will not be compiled nor called into Release configuration. +// In Debug configuration, diagnostics code can be enabled by defining DEBUGNAVIGATOR below, +// but by default nothing is writted, unless some lines are un-commented in Debug(...) below. +// +// Beware! Diagnostics are extremely verbose and can overflow log4net pretty easily. + +#if DEBUG +// define to enable diagnostics code +#undef DEBUGNAVIGATOR +#endif + +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -22,7 +36,7 @@ namespace Umbraco.Core.Xml.XPath // "When an element or attribute name occurs multiple times in an XML document, // it is stored only once in the NameTable. The names are stored as common // language runtime (CLR) object types. This enables you to do object comparisons - // on these strings rather than a more expensive string comparison. These + // on these strings rather than a more expensive string comparison. These // string objects are referred to as atomized strings." // // But... "Any instance members are not guaranteed to be thread safe." @@ -46,55 +60,87 @@ namespace Umbraco.Core.Xml.XPath private readonly INavigableSource _source; private readonly int _lastAttributeIndex; // last index of attributes in the fields collection private State _state; + private readonly int _maxDepth; #region Constructor - /// - /// Initializes a new instance of the class with a content source. - /// - private NavigableNavigator(INavigableSource source) - { - _source = source; - _lastAttributeIndex = source.LastAttributeIndex; - } + ///// + ///// Initializes a new instance of the class with a content source. + ///// + ///// The content source. + ///// The maximum depth. + //private NavigableNavigator(INavigableSource source, int maxDepth) + //{ + // _source = source; + // _lastAttributeIndex = source.LastAttributeIndex; + // _maxDepth = maxDepth; + //} /// /// Initializes a new instance of the class with a content source, /// and an optional root content. /// /// The content source. - /// The root content. + /// The root content identifier. + /// The maximum depth. /// When no root content is supplied then the root of the source is used. - public NavigableNavigator(INavigableSource source, INavigableContent content = null) - : this(source) + public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue) + //: this(source, maxDepth) { + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + _maxDepth = maxDepth; + _nameTable = new NameTable(); _lastAttributeIndex = source.LastAttributeIndex; - _state = new State(content ?? source.Root, null, null, 0, StatePosition.Root); + var content = rootId <= 0 ? source.Root : source.Get(rootId); + if (content == null) + throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId)); + _state = new State(content, null, null, 0, StatePosition.Root); + + _contents = new ConcurrentDictionary(); } + ///// + ///// Initializes a new instance of the class with a content source, a name table and a state. + ///// + ///// The content source. + ///// The name table. + ///// The state. + ///// The maximum depth. + ///// Privately used for cloning a navigator. + //private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth) + // : this(source, rootId: 0, maxDepth: maxDepth) + //{ + // _nameTable = nameTable; + // _state = state; + //} + /// - /// Initializes a new instance of the class with a content source, a name table and a state. + /// Initializes a new instance of the class as a clone. /// - /// The content source. - /// The name table. - /// The state. + /// The cloned navigator. + /// The clone state. + /// The clone maximum depth. /// Privately used for cloning a navigator. - private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state) - : this(source) + private NavigableNavigator(NavigableNavigator orig, State state = null, int maxDepth = -1) + : this(orig._source, rootId: 0, maxDepth: orig._maxDepth) { - _nameTable = nameTable; - _state = state; + _nameTable = orig._nameTable; + + _state = state ?? orig._state.Clone(); + if (state != null && maxDepth < 0) + throw new ArgumentException("Both state and maxDepth are required."); + _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth; + + _contents = orig._contents; } #endregion #region Diagnostics - // diagnostics code will not be compiled nor called into Release configuration. - // in Debug configuration, uncomment lines in Debug() to write to console or to log. - -#if DEBUG +#if DEBUGNAVIGATOR private const string Tabs = " "; private int _tabs; private readonly int _uid = GetUid(); @@ -109,10 +155,15 @@ namespace Umbraco.Core.Xml.XPath } #endif - [Conditional("DEBUG")] + // About conditional methods: marking a method with the [Conditional] attribute ensures + // that no calls to the method will be generated by the compiler. However, the method + // does exist. Wrapping the method body with #if/endif ensures that no IL is generated + // and so it's only an empty method. + + [Conditional("DEBUGNAVIGATOR")] void DebugEnter(string name) { -#if DEBUG +#if DEBUGNAVIGATOR Debug(""); DebugState(":"); Debug(name); @@ -120,45 +171,45 @@ namespace Umbraco.Core.Xml.XPath #endif } - [Conditional("DEBUG")] + [Conditional("DEBUGNAVIGATOR")] void DebugCreate(NavigableNavigator nav) { -#if DEBUG +#if DEBUGNAVIGATOR Debug("Create: [NavigableNavigator::{0}]", nav._uid); #endif } - [Conditional("DEBUG")] + [Conditional("DEBUGNAVIGATOR")] private void DebugReturn() { -#if DEBUG +#if DEBUGNAVIGATOR // ReSharper disable IntroduceOptionalParameters.Local DebugReturn("(void)"); // ReSharper restore IntroduceOptionalParameters.Local #endif } - [Conditional("DEBUG")] + [Conditional("DEBUGNAVIGATOR")] private void DebugReturn(bool value) { -#if DEBUG +#if DEBUGNAVIGATOR DebugReturn(value ? "true" : "false"); #endif } - [Conditional("DEBUG")] + [Conditional("DEBUGNAVIGATOR")] void DebugReturn(string format, params object[] args) { -#if DEBUG +#if DEBUGNAVIGATOR Debug("=> " + format, args); if (_tabs > 0) _tabs -= 2; #endif } - [Conditional("DEBUG")] + [Conditional("DEBUGNAVIGATOR")] void DebugState(string s = " =>") { -#if DEBUG +#if DEBUGNAVIGATOR string position; switch (_state.Position) @@ -169,8 +220,8 @@ namespace Umbraco.Core.Xml.XPath _state.FieldIndex < 0 ? "id" : _state.CurrentFieldType.Name); break; case StatePosition.Element: - position = string.Format("At element '{0}'.", - _state.Content.Type.Name); + position = string.Format("At element '{0}' (depth={1}).", + _state.Content.Type.Name, _state.Depth); break; case StatePosition.PropertyElement: position = string.Format("At property '{0}/{1}'.", @@ -185,7 +236,8 @@ namespace Umbraco.Core.Xml.XPath _state.Content.Type.Name, _state.CurrentFieldType.Name); break; case StatePosition.Root: - position = "At root."; + position = string.Format("At root (depth={0}).", + _state.Depth); break; default: throw new InvalidOperationException("Invalid position."); @@ -195,15 +247,13 @@ namespace Umbraco.Core.Xml.XPath #endif } -#if DEBUG +#if DEBUGNAVIGATOR void Debug(string format, params object[] args) { // remove comments to write - format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; -#pragma warning disable 168 - var msg = string.Format(format, args); // unused if not writing, hence #pragma -#pragma warning restore 168 + //format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; + //var msg = string.Format(format, args); //LogHelper.Debug(msg); // beware! this can quicky overflow log4net //Console.WriteLine(msg); } @@ -211,13 +261,25 @@ namespace Umbraco.Core.Xml.XPath #endregion + #region Source management + + private readonly ConcurrentDictionary _contents; + + private INavigableContent SourceGet(int id) + { + // original version, would keep creating INavigableContent objects + //return _source.Get(id); + + // improved version, uses a cache, shared with clones + return _contents.GetOrAdd(id, x => _source.Get(x)); + } + + #endregion + /// /// Gets the underlying content object. /// - public override object UnderlyingObject - { - get { return _state.Content; } - } + public override object UnderlyingObject => _state.Content; /// /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. @@ -226,7 +288,7 @@ namespace Umbraco.Core.Xml.XPath public override XPathNavigator Clone() { DebugEnter("Clone"); - var nav = new NavigableNavigator(_source, _nameTable, _state.Clone()); + var nav = new NavigableNavigator(this); DebugCreate(nav); DebugReturn("[XPathNavigator]"); return nav; @@ -237,20 +299,32 @@ namespace Umbraco.Core.Xml.XPath /// /// A new XPathNavigator using the same source and positioned at a new root. /// The new root can be above this navigator's root. - public XPathNavigator CloneWithNewRoot(string id) + public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue) + { + int i; + if (int.TryParse(id, out i) == false) + throw new ArgumentException("Not a valid identifier.", nameof(id)); + return CloneWithNewRoot(id); + } + + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator CloneWithNewRoot(int id, int maxDepth = int.MaxValue) { DebugEnter("CloneWithNewRoot"); - int contentId; State state = null; - if (id != null && id.Trim() == "-1") + if (id <= 0) { state = new State(_source.Root, null, null, 0, StatePosition.Root); } - else if (int.TryParse(id, out contentId)) + else { - var content = _source.Get(contentId); + var content = SourceGet(id); if (content != null) { state = new State(content, null, null, 0, StatePosition.Root); @@ -261,13 +335,13 @@ namespace Umbraco.Core.Xml.XPath if (state != null) { - clone = new NavigableNavigator(_source, _nameTable, state); + clone = new NavigableNavigator(this, state, maxDepth); DebugCreate(clone); DebugReturn("[XPathNavigator]"); } else { - DebugReturn("[null]"); + DebugReturn("[null]"); } return clone; @@ -278,7 +352,7 @@ namespace Umbraco.Core.Xml.XPath /// public override bool IsEmptyElement { - get + get { DebugEnter("IsEmptyElement"); bool isEmpty; @@ -286,7 +360,10 @@ namespace Umbraco.Core.Xml.XPath switch (_state.Position) { case StatePosition.Element: - isEmpty = (_state.Content.ChildIds == null || _state.Content.ChildIds.Count == 0) // no content child + // must go through source because of preview/published ie there may be + // ids but corresponding to preview elements that we don't see here + var hasContentChild = _state.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null); + isEmpty = (hasContentChild == false) // no content child && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child break; case StatePosition.PropertyElement: @@ -501,14 +578,14 @@ namespace Umbraco.Core.Xml.XPath private bool MoveToFirstChildElement() { - var children = _state.Content.ChildIds; + var children = _state.GetContentChildIds(_maxDepth); - if (children != null && children.Count > 0) + if (children.Count > 0) { // children may contain IDs that does not correspond to some content in source // because children contains all child IDs including unpublished children - and // then if we're not previewing, the source will return null. - var child = children.Select(id => _source.Get(id)).FirstOrDefault(c => c != null); + var child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); if (child != null) { _state.Position = StatePosition.Element; @@ -544,7 +621,7 @@ namespace Umbraco.Core.Xml.XPath if (valueForXPath == null) return false; - + if (valueForXPath is string) { _state.Position = StatePosition.PropertyText; @@ -596,38 +673,45 @@ namespace Umbraco.Core.Xml.XPath // not sure we actually need to implement it... think of it as // as exercise of style, always better than throwing NotImplemented. - int contentId; - if (/*id != null &&*/ id.Trim() == "-1") // id cannot be null - { - _state = new State(_source.Root, null, _source.Root.ChildIds, 0, StatePosition.Element); - succ = true; - } - else if (int.TryParse(id, out contentId)) - { - var content = _source.Get(contentId); - if (content != null) - { - var state = _state; - while (state.Parent != null) - state = state.Parent; - var navRootId = state.Content.Id; // navigator may be rooted below source root + // navigator may be rooted below source root + // find the navigator root id + var state = _state; + while (state.Parent != null) // root state has no parent + state = state.Parent; + var navRootId = state.Content.Id; - var s = new Stack(); - while (content != null && content.ParentId != navRootId) - { - s.Push(content); - content = _source.Get(content.ParentId); - } + int contentId; + if (int.TryParse(id, out contentId)) + { + if (contentId == navRootId) + { + _state = new State(state.Content, null, null, 0, StatePosition.Element); + succ = true; + } + else + { + var content = SourceGet(contentId); if (content != null) { - _state = new State(_source.Root, null, _source.Root.ChildIds, _source.Root.ChildIds.IndexOf(content.Id), StatePosition.Element); - while (content != null) + // walk up to the navigator's root - or the source's root + var s = new Stack(); + while (content != null && content.ParentId != navRootId) { - _state = new State(content, _state, content.ChildIds, _state.Content.ChildIds.IndexOf(content.Id), StatePosition.Element); - content = s.Count == 0 ? null : s.Pop(); + s.Push(content); + content = SourceGet(content.ParentId); + } + + if (content != null && s.Count < _maxDepth) + { + _state = new State(state.Content, null, null, 0, StatePosition.Element); + while (content != null) + { + _state = new State(content, _state, _state.Content.ChildIds, _state.Content.ChildIds.IndexOf(content.Id), StatePosition.Element); + content = s.Count == 0 ? null : s.Pop(); + } + DebugState(); + succ = true; } - DebugState(); - succ = true; } } } @@ -659,7 +743,7 @@ namespace Umbraco.Core.Xml.XPath // Siblings may contain IDs that does not correspond to some content in source // because children contains all child IDs including unpublished children - and // then if we're not previewing, the source will return null. - var node = _source.Get(_state.Siblings[++_state.SiblingIndex]); + var node = SourceGet(_state.Siblings[++_state.SiblingIndex]); if (node == null) continue; _state.Content = node; @@ -718,7 +802,7 @@ namespace Umbraco.Core.Xml.XPath // children may contain IDs that does not correspond to some content in source // because children contains all child IDs including unpublished children - and // then if we're not previewing, the source will return null. - var content = _source.Get(_state.Siblings[--_state.SiblingIndex]); + var content = SourceGet(_state.Siblings[--_state.SiblingIndex]); if (content == null) continue; _state.Content = content; @@ -735,7 +819,7 @@ namespace Umbraco.Core.Xml.XPath DebugState(); succ = true; } - } + } break; case StatePosition.PropertyElement: succ = false; @@ -819,7 +903,7 @@ namespace Umbraco.Core.Xml.XPath break; case StatePosition.Element: succ = MoveToParentElement(); - if (!succ) + if (succ == false) { _state.Position = StatePosition.Root; succ = true; @@ -831,7 +915,7 @@ namespace Umbraco.Core.Xml.XPath succ = true; break; case StatePosition.PropertyXml: - if (!_state.XmlFragmentNavigator.MoveToParent()) + if (_state.XmlFragmentNavigator.MoveToParent() == false) throw new InvalidOperationException("Could not move to parent in fragment."); if (_state.XmlFragmentNavigator.NodeType == XPathNodeType.Root) { @@ -882,26 +966,17 @@ namespace Umbraco.Core.Xml.XPath /// /// Gets the base URI for the current node. /// - public override string BaseURI - { - get { return string.Empty; } - } + public override string BaseURI => string.Empty; /// /// Gets the XmlNameTable of the XPathNavigator. /// - public override XmlNameTable NameTable - { - get { return _nameTable; } - } + public override XmlNameTable NameTable => _nameTable; /// /// Gets the namespace URI of the current node. /// - public override string NamespaceURI - { - get { return string.Empty; } - } + public override string NamespaceURI => string.Empty; /// /// Gets the XPathNodeType of the current node. @@ -943,10 +1018,7 @@ namespace Umbraco.Core.Xml.XPath /// /// Gets the namespace prefix associated with the current node. /// - public override string Prefix - { - get { return string.Empty; } - } + public override string Prefix => string.Empty; /// /// Gets the string value of the item. @@ -981,7 +1053,7 @@ namespace Umbraco.Core.Xml.XPath // - an XPathNavigator over a non-empty XML fragment // - a non-Xml-whitespace string // - null - + var nav = valueForXPath as XPathNavigator; var s = valueForXPath as string; if (valueForXPath == null) @@ -1031,7 +1103,7 @@ namespace Umbraco.Core.Xml.XPath // gets the state // for unit tests only - internal State InternalState { get { return _state; } } + internal State InternalState => _state; // represents the XPathNavigator state internal class State @@ -1053,6 +1125,7 @@ namespace Umbraco.Core.Xml.XPath { Content = content; Parent = parent; + Depth = parent?.Depth + 1 ?? 0; Siblings = siblings; SiblingIndex = siblingIndex; } @@ -1067,6 +1140,7 @@ namespace Umbraco.Core.Xml.XPath Siblings = other.Siblings; FieldsCount = other.FieldsCount; FieldIndex = other.FieldIndex; + Depth = other.Depth; if (Position == StatePosition.PropertyXml) XmlFragmentNavigator = other.XmlFragmentNavigator.Clone(); @@ -1096,6 +1170,9 @@ namespace Umbraco.Core.Xml.XPath // the parent state public State Parent { get; private set; } + // the depth + public int Depth { get; } + // the current content private INavigableContent _content; @@ -1108,16 +1185,24 @@ namespace Umbraco.Core.Xml.XPath } set { - FieldsCount = value == null ? 0 : value.Type.FieldTypes.Length; + FieldsCount = value?.Type.FieldTypes.Length ?? 0; _content = value; } } + private static readonly int[] NoChildIds = new int[0]; + + // the current content child ids + public IList GetContentChildIds(int maxDepth) + { + return Depth < maxDepth && _content.ChildIds != null ? _content.ChildIds : NoChildIds; + } + // the index of the current content within Siblings public int SiblingIndex { get; set; } // the list of content identifiers for all children of the current content's parent - public IList Siblings { get; set; } + public IList Siblings { get; } // the number of fields of the current content // properties include attributes and properties @@ -1129,7 +1214,7 @@ namespace Umbraco.Core.Xml.XPath // the current field type // beware, no check on the index - public INavigableFieldType CurrentFieldType { get { return Content.Type.FieldTypes[FieldIndex]; } } + public INavigableFieldType CurrentFieldType => Content.Type.FieldTypes[FieldIndex]; // gets or sets the xml fragment navigator public XPathNavigator XmlFragmentNavigator { get; set; } @@ -1137,9 +1222,9 @@ namespace Umbraco.Core.Xml.XPath // gets a value indicating whether this state is at the same position as another one. public bool IsSamePosition(State other) { - return other.Position == Position + return other.Position == Position && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) - && other.Content == Content + && other.Content == Content && other.FieldIndex == FieldIndex; } } diff --git a/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs b/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs index d4520e378c..483dfad7ed 100644 --- a/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs @@ -103,33 +103,23 @@ namespace Umbraco.Tests.Cache.DistributedCache internal class TestCacheRefresher : ICacheRefresher { - public Guid UniqueIdentifier - { - get { return Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"); } - } - public string Name - { - get { return "Test"; } - } + public static readonly Guid UniqueId = Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"); + + public Guid RefresherUniqueId => UniqueId; + + public string Name => "Test Cache Refresher"; + public void RefreshAll() - { - - } + { } public void Refresh(int id) - { - - } + { } public void Remove(int id) - { - - } + { } public void Refresh(Guid id) - { - - } + { } } internal class TestServerMessenger : IServerMessenger @@ -142,7 +132,7 @@ namespace Umbraco.Tests.Cache.DistributedCache public List PayloadsRefreshed = new List(); public int CountOfFullRefreshes = 0; - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, object payload) + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, TPayload[] payload) { // doing nothing } diff --git a/src/Umbraco.Tests/LibraryTests.cs b/src/Umbraco.Tests/LibraryTests.cs index 2039192d7d..3d982d3f90 100644 --- a/src/Umbraco.Tests/LibraryTests.cs +++ b/src/Umbraco.Tests/LibraryTests.cs @@ -41,9 +41,9 @@ namespace Umbraco.Tests new PublishedPropertyType("content", 0, "?"), }; var type = new AutoPublishedContentType(0, "anything", propertyTypes); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; Console.WriteLine("INIT LIB {0}", - PublishedContentType.Get(PublishedItemType.Content, "anything") + ContentTypesCache.Get(PublishedItemType.Content, "anything") .PropertyTypes.Count()); var routingContext = GetRoutingContext("/test", 1234); @@ -158,9 +158,9 @@ namespace Umbraco.Tests /// private string LegacyGetItem(int nodeId, string alias) { - var cache = UmbracoContext.Current.ContentCache.InnerCache as PublishedContentCache; + var cache = UmbracoContext.Current.ContentCache as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); - var umbracoXML = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); + var umbracoXML = cache.GetXml(UmbracoContext.Current.InPreviewMode); string xpath = "./{0}"; if (umbracoXML.GetElementById(nodeId.ToString()) != null) diff --git a/src/Umbraco.Tests/Membership/DynamicMemberContentTests.cs b/src/Umbraco.Tests/Membership/DynamicMemberContentTests.cs index 4c1272550d..6b8fd17a2d 100644 --- a/src/Umbraco.Tests/Membership/DynamicMemberContentTests.cs +++ b/src/Umbraco.Tests/Membership/DynamicMemberContentTests.cs @@ -42,8 +42,7 @@ namespace Umbraco.Tests.Membership new PublishedPropertyType("author", 0, "?") }; var type = new AutoPublishedContentType(0, "anything", propertyTypes); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; - + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; } [Test] @@ -64,7 +63,8 @@ namespace Umbraco.Tests.Membership member.LastPasswordChangeDate = date.AddMinutes(4); member.PasswordQuestion = "test question"; - var mpc = new MemberPublishedContent(member); + var mpt = ContentTypesCache.Get(PublishedItemType.Member, member.ContentTypeAlias); + var mpc = new PublishedMember(member, mpt); var d = mpc.AsDynamic(); @@ -101,7 +101,8 @@ namespace Umbraco.Tests.Membership member.LastPasswordChangeDate = date.AddMinutes(4); member.PasswordQuestion = "test question"; - var mpc = new MemberPublishedContent(member); + var mpt = ContentTypesCache.Get(PublishedItemType.Member, member.ContentTypeAlias); + var mpc = new PublishedMember(member, mpt); var d = mpc.AsDynamic(); @@ -141,7 +142,9 @@ namespace Umbraco.Tests.Membership member.Properties["title"].Value = "Test Value 1"; member.Properties["bodyText"].Value = "Test Value 2"; member.Properties["author"].Value = "Test Value 3"; - var mpc = new MemberPublishedContent(member); + + var mpt = ContentTypesCache.Get(PublishedItemType.Member, member.ContentTypeAlias); + var mpc = new PublishedMember(member, mpt); var d = mpc.AsDynamic(); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 32d1a8a782..a7ebb2f7cb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -57,177 +57,6 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } - [Test] - public void Rebuild_Xml_Structures_With_Non_Latest_Version() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - ContentTypeRepository contentTypeRepository; - var repository = CreateRepository(unitOfWork, out contentTypeRepository); - - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - - var allCreated = new List(); - - //create 100 non published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - //create 100 published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Publishing); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Flush(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (var i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - //IMPORTANT testing note here: We need to changed the published state here so that - // it doesn't automatically think this is simply publishing again - this forces the latest - // version to be Saved and not published - allCreated[i].ChangePublishedState(PublishedState.Saving); - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(content => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - unitOfWork.Complete(); - } - } - - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - ContentTypeRepository contentTypeRepository; - var repository = CreateRepository(unitOfWork, out contentTypeRepository); - - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - var allCreated = new List(); - - for (var i = 0; i < 100; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Publishing); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Flush(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - unitOfWork.Complete(); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - ContentTypeRepository contentTypeRepository; - var repository = CreateRepository(unitOfWork, out contentTypeRepository); - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - var contentType2 = MockedContentTypes.CreateSimpleContentType("Textpage2", "Textpage2"); - var contentType3 = MockedContentTypes.CreateSimpleContentType("Textpage3", "Textpage3"); - contentTypeRepository.AddOrUpdate(contentType1); - contentTypeRepository.AddOrUpdate(contentType2); - contentTypeRepository.AddOrUpdate(contentType3); - - var allCreated = new List(); - - for (var i = 0; i < 30; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Publishing); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType2); - c1.ChangePublishedState(PublishedState.Publishing); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType3); - c1.ChangePublishedState(PublishedState.Publishing); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Flush(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { contentType1.Id, contentType2.Id }); - - Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - unitOfWork.Complete(); - } - } - [Test] public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 000817cd5c..0fb798c1b3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -41,74 +41,6 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - MediaTypeRepository mediaTypeRepository; - var repository = CreateRepository(unitOfWork, out mediaTypeRepository); - - var mediaType = mediaTypeRepository.Get(1032); - - for (var i = 0; i < 100; i++) - { - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - MediaTypeRepository mediaTypeRepository; - var repository = CreateRepository(unitOfWork, out mediaTypeRepository); - - var imageMediaType = mediaTypeRepository.Get(1032); - var fileMediaType = mediaTypeRepository.Get(1033); - var folderMediaType = mediaTypeRepository.Get(1031); - - for (var i = 0; i < 30; i++) - { - var image = MockedMedia.CreateMediaImage(imageMediaType, -1); - repository.AddOrUpdate(image); - } - for (var i = 0; i < 30; i++) - { - var file = MockedMedia.CreateMediaFile(fileMediaType, -1); - repository.AddOrUpdate(file); - } - for (var i = 0; i < 30; i++) - { - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - repository.AddOrUpdate(folder); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { 1032, 1033 }); - - Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - [Test] public void Can_Perform_Add_On_MediaRepository() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index b9d20b1261..4bc5f2862c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -44,78 +44,6 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - MemberTypeRepository memberTypeRepository; - MemberGroupRepository memberGroupRepository; - var repository = CreateRepository(unitOfWork, out memberTypeRepository, out memberGroupRepository); - - var memberType1 = CreateTestMemberType(); - - for (var i = 0; i < 100; i++) - { - var member = MockedMember.CreateSimpleMember(memberType1, "blah" + i, "blah" + i + "@example.com", "blah", "blah" + i); - repository.AddOrUpdate(member); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var unitOfWork = provider.CreateUnitOfWork()) - { - MemberTypeRepository memberTypeRepository; - MemberGroupRepository memberGroupRepository; - var repository = CreateRepository(unitOfWork, out memberTypeRepository, out memberGroupRepository); - - var memberType1 = CreateTestMemberType("mt1"); - var memberType2 = CreateTestMemberType("mt2"); - var memberType3 = CreateTestMemberType("mt3"); - - for (var i = 0; i < 30; i++) - { - var member = MockedMember.CreateSimpleMember(memberType1, "b1lah" + i, "b1lah" + i + "@example.com", "b1lah", "b1lah" + i); - repository.AddOrUpdate(member); - } - for (var i = 0; i < 30; i++) - { - var member = MockedMember.CreateSimpleMember(memberType2, "b2lah" + i, "b2lah" + i + "@example.com", "b2lah", "b2lah" + i); - repository.AddOrUpdate(member); - } - for (var i = 0; i < 30; i++) - { - var member = MockedMember.CreateSimpleMember(memberType3, "b3lah" + i, "b3lah" + i + "@example.com", "b3lah", "b3lah" + i); - repository.AddOrUpdate(member); - } - unitOfWork.Flush(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { memberType1.Id, memberType2.Id }); - - Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - - [Test] public void MemberRepository_Can_Get_Member_By_Id() { diff --git a/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs b/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs index 4e9a2590b0..613ada78d8 100644 --- a/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs +++ b/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.PublishedContent new PublishedPropertyType("blah", 0, "?"), // ugly error when that one is missing... }; var type = new AutoPublishedContentType(0, "anything", propertyTypes); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 8efe6efd72..d2a9b7246a 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -22,11 +22,6 @@ namespace Umbraco.Tests.PublishedContent { base.Initialize(); - // need to specify a custom callback for unit tests - // AutoPublishedContentTypes generates properties automatically - var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] {}); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; - // need to specify a different callback for testing PublishedContentExtensions.GetPropertyAliasesAndNames = s => { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index dd7c1d80ac..1bc426b0db 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -1,16 +1,9 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Tests.TestHelpers; using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; namespace Umbraco.Tests.PublishedContent { @@ -30,7 +23,7 @@ namespace Umbraco.Tests.PublishedContent new PublishedPropertyType("content", 0, Constants.PropertyEditors.TinyMCEAlias), }; var type = new AutoPublishedContentType(0, "anything", propertyTypes); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; var rCtx = GetRoutingContext("/test", 1234); UmbracoContext.Current = rCtx.UmbracoContext; @@ -49,9 +42,6 @@ namespace Umbraco.Tests.PublishedContent typeof(YesNoValueConverter) }); - PublishedCachesResolver.Current = new PublishedCachesResolver(new PublishedCaches( - new PublishedContentCache(), new PublishedMediaCache(ApplicationContext))); - if (PublishedContentModelFactoryResolver.HasCurrent == false) PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 0608039437..b083fa60eb 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Web; @@ -7,8 +6,6 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Plugins; -using Umbraco.Core.PropertyEditors; -using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.Models; @@ -45,15 +42,15 @@ namespace Umbraco.Tests.PublishedContent var propertyTypes = new[] { // AutoPublishedContentType will auto-generate other properties - new PublishedPropertyType("umbracoNaviHide", 0, Constants.PropertyEditors.TrueFalseAlias), - new PublishedPropertyType("selectedNodes", 0, "?"), - new PublishedPropertyType("umbracoUrlAlias", 0, "?"), - new PublishedPropertyType("content", 0, Constants.PropertyEditors.TinyMCEAlias), - new PublishedPropertyType("testRecursive", 0, "?"), + new PublishedPropertyType("umbracoNaviHide", 0, Constants.PropertyEditors.TrueFalseAlias), + new PublishedPropertyType("selectedNodes", 0, "?"), + new PublishedPropertyType("umbracoUrlAlias", 0, "?"), + new PublishedPropertyType("content", 0, Constants.PropertyEditors.TinyMCEAlias), + new PublishedPropertyType("testRecursive", 0, "?"), }; var compositionAliases = new[] {"MyCompositionAlias"}; var type = new AutoPublishedContentType(0, "anything", compositionAliases, propertyTypes); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; } public override void TearDown() @@ -74,7 +71,7 @@ namespace Umbraco.Tests.PublishedContent protected override string GetXmlContent(int templateId) { return @" - @@ -88,14 +85,14 @@ namespace Umbraco.Tests.PublishedContent This is some content]]> - + - + @@ -212,8 +209,8 @@ namespace Umbraco.Tests.PublishedContent [PublishedContentModel("Home")] internal class Home : PublishedContentModel { - public Home(IPublishedContent content) - : base(content) + public Home(IPublishedContent content) + : base(content) {} } @@ -672,7 +669,7 @@ namespace Umbraco.Tests.PublishedContent public void CreateDetachedContentSample() { bool previewing = false; - var t = PublishedContentType.Get(PublishedItemType.Content, "detachedSomething"); + var t = ContentTypesCache.Get(PublishedItemType.Content, "detachedSomething"); var values = new Dictionary(); var properties = t.PropertyTypes.Select(x => { diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 0405e3c3dc..9d3740ec19 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -28,6 +28,7 @@ namespace Umbraco.Tests.Services /// [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] + [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class ContentServiceTests : BaseServiceTest { [SetUp] @@ -883,106 +884,6 @@ namespace Umbraco.Tests.Services } } - /// - /// This test is ignored because the way children are handled when - /// parent is unpublished is treated differently now then from when this test - /// was written. - /// The correct case is now that Root is UnPublished removing the children - /// from cache, but still having them "Published" in the "background". - /// Once the Parent is Published the Children should re-appear as published. - /// - [Test, NUnit.Framework.Ignore] - public void Can_UnPublish_Root_Content_And_Verify_Children_Is_UnPublished() - { - // Arrange - var contentService = ServiceContext.ContentService; - var published = contentService.RePublishAll(0); - var content = contentService.GetById(NodeDto.NodeIdSeed + 1); - - // Act - bool unpublished = contentService.UnPublish(content, 0); - var children = contentService.GetChildren(NodeDto.NodeIdSeed + 1).ToList(); - - // Assert - Assert.That(published, Is.True);//Verify that everything was published - - //Verify that content with Id (NodeDto.NodeIdSeed + 1) was unpublished - Assert.That(unpublished, Is.True); - Assert.That(content.Published, Is.False); - - //Verify that all children was unpublished - Assert.That(children.Any(x => x.Published), Is.False); - Assert.That(children.First(x => x.Id == NodeDto.NodeIdSeed + 2).Published, Is.False);//Released 5 mins ago, but should be unpublished - Assert.That(children.First(x => x.Id == NodeDto.NodeIdSeed + 2).ReleaseDate.HasValue, Is.False);//Verify that the release date has been removed - Assert.That(children.First(x => x.Id == NodeDto.NodeIdSeed + 3).Published, Is.False);//Expired 5 mins ago, so isn't be published - } - - [Test] - public void Can_RePublish_All_Content() - { - // Arrange - var contentService = (ContentService)ServiceContext.ContentService; - var rootContent = contentService.GetRootContent().ToList(); - foreach (var c in rootContent) - { - contentService.PublishWithChildren(c); - } - var allContent = rootContent.Concat(rootContent.SelectMany(x => x.Descendants(contentService))); - //for testing we need to clear out the contentXml table so we can see if it worked - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - using (var uow = provider.CreateUnitOfWork()) - { - uow.Database.TruncateTable(SqlSyntax, "cmsContentXml"); - } - - - //for this test we are also going to save a revision for a content item that is not published, this is to ensure - //that it's published version still makes it into the cmsContentXml table! - contentService.Save(allContent.Last()); - - // Act - var published = contentService.RePublishAll(0); - - // Assert - Assert.IsTrue(published); - using (var uow = provider.CreateUnitOfWork()) - { - Assert.AreEqual(allContent.Count(), uow.Database.ExecuteScalar("select count(*) from cmsContentXml")); - } - } - - [Test] - public void Can_RePublish_All_Content_Of_Type() - { - // Arrange - var contentService = (ContentService)ServiceContext.ContentService; - var rootContent = contentService.GetRootContent().ToList(); - foreach (var c in rootContent) - { - contentService.PublishWithChildren(c); - } - var allContent = rootContent.Concat(rootContent.SelectMany(x => x.Descendants(contentService))).ToList(); - //for testing we need to clear out the contentXml table so we can see if it worked - var provider = TestObjects.GetDatabaseUnitOfWorkProvider(Logger); - - using (var uow = provider.CreateUnitOfWork()) - { - uow.Database.TruncateTable(SqlSyntax, "cmsContentXml"); - } - //for this test we are also going to save a revision for a content item that is not published, this is to ensure - //that it's published version still makes it into the cmsContentXml table! - contentService.Save(allContent.Last()); - - // Act - contentService.RePublishAll(new int[]{allContent.Last().ContentTypeId}); - - // Assert - using (var uow = provider.CreateUnitOfWork()) - { - Assert.AreEqual(allContent.Count(), uow.Database.ExecuteScalar("select count(*) from cmsContentXml")); - } - } - [Test] public void Can_Publish_Content() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index 0c885e96f4..d9ff7f5c7c 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -16,6 +16,7 @@ namespace Umbraco.Tests.Services [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] + [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class ContentTypeServiceTests : BaseServiceTest { [SetUp] diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index fba29ea369..951eef0dc7 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -15,6 +15,7 @@ namespace Umbraco.Tests.Services { [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] + [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class MediaServiceTests : BaseServiceTest { [SetUp] diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index 1a2ec8e516..8805eebd0b 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -20,6 +20,7 @@ namespace Umbraco.Tests.Services { [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] + [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class MemberServiceTests : BaseServiceTest { [SetUp] diff --git a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs index c81d8c3f4e..947d0eafff 100644 --- a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs @@ -13,6 +13,7 @@ namespace Umbraco.Tests.Services { [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] + [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class MemberTypeServiceTests : BaseServiceTest { [SetUp] @@ -150,7 +151,7 @@ namespace Umbraco.Tests.Services var alias = contentType1.PropertyTypes.First(x => standardProps.ContainsKey(x.Alias) == false).Alias; var elementToMatch = "<" + alias + ">"; - + foreach (var c in contentItems1) { var xml = DatabaseContext.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }); @@ -158,7 +159,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(xml.Xml.Contains(elementToMatch)); //verify that it is there before we remove the property } - //remove a property (NOT ONE OF THE DEFAULTS) + //remove a property (NOT ONE OF THE DEFAULTS) contentType1.RemovePropertyType(alias); ServiceContext.MemberTypeService.Save(contentType1); diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index c1ef20f0b4..e85f0b370c 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -27,6 +27,7 @@ using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Security; using Umbraco.Core.Events; using Umbraco.Core.Plugins; +using Umbraco.Web.Routing; using File = System.IO.File; namespace Umbraco.Tests.TestHelpers @@ -38,6 +39,8 @@ namespace Umbraco.Tests.TestHelpers [TestFixture, RequiresSTA] public abstract class BaseDatabaseFactoryTest : BaseUmbracoApplicationTest { + protected PublishedContentTypeCache ContentTypesCache; + //This is used to indicate that this is the first test to run in the test session, if so, we always //ensure a new database file is used. private static volatile bool _firstRunInTestSession = true; @@ -45,15 +48,17 @@ namespace Umbraco.Tests.TestHelpers private bool _firstTestInFixture = true; //Used to flag if its the first test in the current session - private bool _isFirstRunInTestSession = false; + private bool _isFirstRunInTestSession; //Used to flag if its the first test in the current fixture - private bool _isFirstTestInFixture = false; + private bool _isFirstTestInFixture; private ApplicationContext _appContext; + private IFacadeService _facadeService; + private IDatabaseUnitOfWorkProvider _uowProvider; private string _dbPath; //used to store (globally) the pre-built db with schema and initial data - private static Byte[] _dbBytes; + private static byte[] _dbBytes; [SetUp] public override void Initialize() @@ -74,10 +79,7 @@ namespace Umbraco.Tests.TestHelpers } private CacheHelper _disabledCacheHelper; - protected CacheHelper DisabledCache - { - get { return _disabledCacheHelper ?? (_disabledCacheHelper = CacheHelper.CreateDisabledCacheHelper()); } - } + protected CacheHelper DisabledCache => _disabledCacheHelper ?? (_disabledCacheHelper = CacheHelper.CreateDisabledCacheHelper()); protected override void SetupApplicationContext() { @@ -110,7 +112,7 @@ namespace Umbraco.Tests.TestHelpers var repositoryFactory = Container.GetInstance(); var serviceContext = TestObjects.GetServiceContext( repositoryFactory, - new NPocoUnitOfWorkProvider(databaseFactory, repositoryFactory), + _uowProvider = new NPocoUnitOfWorkProvider(databaseFactory, repositoryFactory), new FileUnitOfWorkProvider(), CacheHelper, Logger, @@ -244,6 +246,48 @@ namespace Umbraco.Tests.TestHelpers if (PublishedContentModelFactoryResolver.HasCurrent == false) PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); + // ensure we have a FacadeService + if (_facadeService == null) + { + var behavior = GetType().GetCustomAttribute(false); + var cache = new NullCacheProvider(); + + var enableRepositoryEvents = behavior != null && behavior.EnableRepositoryEvents; + if (enableRepositoryEvents && LoggerResolver.HasCurrent == false) + { + // XmlStore wants one if handling events + LoggerResolver.Current = new LoggerResolver(Mock.Of()) + { + CanResolveBeforeFrozen = true + }; + ProfilerResolver.Current = new ProfilerResolver(new LogProfiler(Mock.Of())) + { + CanResolveBeforeFrozen = true + }; + } + + ContentTypesCache = new PublishedContentTypeCache( + ApplicationContext.Services.ContentTypeService, + ApplicationContext.Services.MediaTypeService, + ApplicationContext.Services.MemberTypeService); + + // testing=true so XmlStore will not use the file nor the database + var service = new FacadeService( + ApplicationContext.Services, + _uowProvider, + cache, ContentTypesCache, true, enableRepositoryEvents); + + // initialize PublishedCacheService content with an Xml source + service.XmlStore.GetXmlDocument = () => + { + var doc = new XmlDocument(); + doc.LoadXml(GetXmlContent(0)); + return doc; + }; + + _facadeService = service; + } + base.FreezeResolution(); } @@ -298,6 +342,8 @@ namespace Umbraco.Tests.TestHelpers AppDomain.CurrentDomain.SetData("DataDirectory", null); + // make sure we dispose of the service to unbind events + _facadeService?.Dispose(); } base.TearDown(); @@ -356,10 +402,7 @@ namespace Umbraco.Tests.TestHelpers LogHelper.Error("Could not remove the old database file", ex); //We will swallow this exception! That's because a sub class might require further teardown logic. - if (onFail != null) - { - onFail(ex); - } + onFail?.Invoke(ex); } } @@ -369,21 +412,28 @@ namespace Umbraco.Tests.TestHelpers protected UmbracoContext GetUmbracoContext(string url, int templateId, RouteData routeData = null, bool setSingleton = false) { - var cache = new PublishedContentCache((context, preview) => + // ensure we have a PublishedCachesService + var service = _facadeService as FacadeService; + if (service == null) + throw new Exception("Not a proper XmlPublishedCache.PublishedCachesService."); + + // re-initialize PublishedCacheService content with an Xml source with proper template id + service.XmlStore.GetXmlDocument = () => { var doc = new XmlDocument(); doc.LoadXml(GetXmlContent(templateId)); return doc; - }); - - PublishedContentCache.UnitTesting = true; + }; var httpContext = GetHttpContextFactory(url, routeData).HttpContext; - var ctx = new UmbracoContext( + var ctx = UmbracoContext.CreateContext( httpContext, ApplicationContext, - new PublishedCaches(cache, new PublishedMediaCache(ApplicationContext)), - new WebSecurity(httpContext, ApplicationContext)); + service, + new WebSecurity(httpContext, ApplicationContext), + null, + Enumerable.Empty(), + null); if (setSingleton) { diff --git a/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs b/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs index 81e85ae2a0..4032fd68c3 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs @@ -1,13 +1,10 @@ -using System.Configuration; using System.Linq; using System.Web.Routing; using NUnit.Framework; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; -using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Routing; namespace Umbraco.Tests.TestHelpers @@ -20,7 +17,7 @@ namespace Umbraco.Tests.TestHelpers /// /// /// - /// The template Id to insert into the Xml cache file for each node, this is helpful for unit testing with templates but you + /// The template Id to insert into the Xml cache file for each node, this is helpful for unit testing with templates but you /// should normally create the template in the database with this id /// /// @@ -73,7 +70,5 @@ namespace Umbraco.Tests.TestHelpers { return GetRoutingContext(url, 1234, routeData); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 49fb51c9d0..ee5cd0efbc 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -16,19 +16,19 @@ namespace Umbraco.Tests.TestHelpers // need to specify a custom callback for unit tests // AutoPublishedContentTypes generates properties automatically var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] {}); - PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + ContentTypesCache.GetPublishedContentTypeByAlias = (alias) => type; } - + [TearDown] public override void TearDown() { base.TearDown(); } - + protected override string GetXmlContent(int templateId) { return @" - @@ -41,7 +41,7 @@ namespace Umbraco.Tests.TestHelpers 1 This is some content]]> - + @@ -59,15 +59,5 @@ namespace Umbraco.Tests.TestHelpers "; } - - /// - /// sets up resolvers before resolution is frozen - /// - protected override void FreezeResolution() - { - - - base.FreezeResolution(); - } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 1804f30a0a..6c3dfb74e3 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -145,12 +145,12 @@ namespace Umbraco.Tests.TestHelpers var userService = new Lazy(() => new UserService(provider, logger, eventMessagesFactory)); var dataTypeService = new Lazy(() => new DataTypeService(provider, logger, eventMessagesFactory)); - var contentService = new Lazy(() => new ContentService(provider, logger, eventMessagesFactory, dataTypeService.Value, userService.Value, urlSegmentProviders)); + var contentService = new Lazy(() => new ContentService(provider, logger, eventMessagesFactory)); var notificationService = new Lazy(() => new NotificationService(provider, userService.Value, contentService.Value, repositoryFactory, logger)); var serverRegistrationService = new Lazy(() => new ServerRegistrationService(provider, logger, eventMessagesFactory)); var memberGroupService = new Lazy(() => new MemberGroupService(provider, logger, eventMessagesFactory)); - var memberService = new Lazy(() => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value, dataTypeService.Value)); - var mediaService = new Lazy(() => new MediaService(provider, logger, eventMessagesFactory, dataTypeService.Value, userService.Value, urlSegmentProviders)); + var memberService = new Lazy(() => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value)); + var mediaService = new Lazy(() => new MediaService(provider, logger, eventMessagesFactory)); var contentTypeService = new Lazy(() => new ContentTypeService(provider, logger, eventMessagesFactory, contentService.Value)); var mediaTypeService = new Lazy(() => new MediaTypeService(provider, logger, eventMessagesFactory, mediaService.Value)); var fileService = new Lazy(() => new FileService(fileProvider, provider, logger, eventMessagesFactory)); diff --git a/src/Umbraco.Tests/TestSetup/FacadeServiceAttribute.cs b/src/Umbraco.Tests/TestSetup/FacadeServiceAttribute.cs new file mode 100644 index 0000000000..fdff6baf88 --- /dev/null +++ b/src/Umbraco.Tests/TestSetup/FacadeServiceAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Tests.TestSetup +{ + [AttributeUsage(AttributeTargets.Class)] + internal sealed class FacadeServiceAttribute : Attribute + { + public bool EnableRepositoryEvents { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 748535773d..efcf6d075a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -229,6 +229,7 @@ + diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 3fb38db592..9e2c84c52d 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + "; + + return div; + } + + private static string EncodeMacroAttribute(string attributeContents) + { + // replace linebreaks + attributeContents = attributeContents.Replace("\n", "\\n").Replace("\r", "\\r"); + + // replace quotes + attributeContents = attributeContents.Replace("\"", """); + + // replace tag start/ends + attributeContents = attributeContents.Replace("<", "<").Replace(">", ">"); + + return attributeContents; + } + + public static string RenderMacroEndTag() + { + return ""; + } + + private static readonly Regex HrefRegex = new Regex("href=\"([^\"]*)\"", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + // fixme - ONLY reference to an old DLL! + public static string GetRenderedMacro(int macroId, global::umbraco.page umbPage, Hashtable attributes, int pageId, ProfilingLogger plogger) + { + var macro = GetMacroModel(macroId); + if (macro == null) return string.Empty; + + // get as text, will render the control if any + var renderer = new MacroRenderer(plogger); + var macroContent = renderer.Render(macro, umbPage.Elements, pageId); + var text = macroContent.GetAsText(); + + // remove hrefs + text = HrefRegex.Replace(text, match => "href=\"javascript:void(0)\""); + + return text; + } + + public static string MacroContentByHttp(int pageId, Guid pageVersion, Hashtable attributes) + { + // though... we only support FullTrust now? + if (SystemUtilities.GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) + return "Cannot render macro content in the rich text editor when the application is running in a Partial Trust environment"; + + var tempAlias = attributes["macroalias"]?.ToString() ?? attributes["macroAlias"].ToString(); + + var macro = GetMacroModel(tempAlias); + if (macro.RenderInEditor == false) + return ShowNoMacroContent(macro); + + var querystring = $"umbPageId={pageId}&umbVersionId={pageVersion}"; + var ide = attributes.GetEnumerator(); + while (ide.MoveNext()) + querystring += $"&umb_{ide.Key}={HttpContext.Current.Server.UrlEncode((ide.Value ?? String.Empty).ToString())}"; + + // create a new 'HttpWebRequest' object to the mentioned URL. + var useSsl = GlobalSettings.UseSSL; + var protocol = useSsl ? "https" : "http"; + var currentRequest = HttpContext.Current.Request; + var serverVars = currentRequest.ServerVariables; + var umbracoDir = IOHelper.ResolveUrl(SystemDirectories.Umbraco); + var url = $"{protocol}://{serverVars["SERVER_NAME"]}:{serverVars["SERVER_PORT"]}{umbracoDir}/macroResultWrapper.aspx?{querystring}"; + + var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); + + // allows for validation of SSL conversations (to bypass SSL errors in debug mode!) + ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate; + + // propagate the user's context + // TODO: this is the worst thing ever. + // also will not work if people decide to put their own custom auth system in place. + var inCookie = currentRequest.Cookies[UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName]; + if (inCookie == null) throw new NullReferenceException("No auth cookie found"); + var cookie = new Cookie(inCookie.Name, inCookie.Value, inCookie.Path, serverVars["SERVER_NAME"]); + myHttpWebRequest.CookieContainer = new CookieContainer(); + myHttpWebRequest.CookieContainer.Add(cookie); + + // assign the response object of 'HttpWebRequest' to a 'HttpWebResponse' variable. + HttpWebResponse myHttpWebResponse = null; + var text = string.Empty; + try + { + myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse(); + if (myHttpWebResponse.StatusCode == HttpStatusCode.OK) + { + var streamResponse = myHttpWebResponse.GetResponseStream(); + if (streamResponse == null) + throw new Exception("Internal error, no response stream."); + var streamRead = new StreamReader(streamResponse); + var readBuff = new char[256]; + var count = streamRead.Read(readBuff, 0, 256); + while (count > 0) + { + var outputData = new string(readBuff, 0, count); + text += outputData; + count = streamRead.Read(readBuff, 0, 256); + } + + streamResponse.Close(); + streamRead.Close(); + + // find the content of a form + const string grabStart = ""; + const string grabEnd = ""; + + var grabStartPos = text.InvariantIndexOf(grabStart) + grabStart.Length; + var grabEndPos = text.InvariantIndexOf(grabEnd) - grabStartPos; + text = text.Substring(grabStartPos, grabEndPos); + } + else + { + text = ShowNoMacroContent(macro); + } + } + catch (Exception) + { + text = ShowNoMacroContent(macro); + } + finally + { + // release the HttpWebResponse Resource. + myHttpWebResponse?.Close(); + } + + return text.Replace("\n", string.Empty).Replace("\r", string.Empty); + } + + private static string ShowNoMacroContent(MacroModel model) + { + // fixme should we html-escape the name? + return $"{model.Name}
No macro content available for WYSIWYG editing
"; + } + + private static bool ValidateRemoteCertificate( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors policyErrors + ) + { + // allow any old dodgy certificate in debug mode + return GlobalSettings.DebugMode || policyErrors == SslPolicyErrors.None; + } + + #endregion + } + + public class MacroRenderingEventArgs : EventArgs + { + public MacroRenderingEventArgs(Hashtable pageElements, int pageId) + { + PageElements = pageElements; + PageId = pageId; + } + + public int PageId { get; } + + public Hashtable PageElements { get; } + } +} diff --git a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs index aee04cfb29..bd18c45749 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs @@ -1,44 +1,38 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using System.Web; using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; -using Umbraco.Core.IO; using umbraco.cms.businesslogic.macro; using Umbraco.Core.Models; -using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Core; -using System.Web.Mvc.Html; namespace Umbraco.Web.Macros { /// - /// A macro engine using MVC Partial Views to execute + /// A macro engine using MVC Partial Views to execute. /// - public class PartialViewMacroEngine + public class PartialViewMacroEngine { private readonly Func _getHttpContext; private readonly Func _getUmbracoContext; - + public PartialViewMacroEngine() { _getHttpContext = () => { if (HttpContext.Current == null) - throw new InvalidOperationException("The " + this.GetType() + " cannot execute with a null HttpContext.Current reference"); + throw new InvalidOperationException($"The {GetType()} cannot execute with a null HttpContext.Current reference."); return new HttpContextWrapper(HttpContext.Current); }; _getUmbracoContext = () => { if (UmbracoContext.Current == null) - throw new InvalidOperationException("The " + this.GetType() + " cannot execute with a null UmbracoContext.Current reference"); + throw new InvalidOperationException($"The {GetType()} cannot execute with a null UmbracoContext.Current reference."); return UmbracoContext.Current; }; } @@ -54,24 +48,6 @@ namespace Umbraco.Web.Macros _getUmbracoContext = () => umbracoContext; } - //NOTE: We do not return any supported extensions because we don't want the MacroEngineFactory to return this - // macro engine when searching for engines via extension. Those types of engines are reserved for files that are - // stored in the ~/macroScripts folder and each engine must support unique extensions. This is a total Hack until - // we rewrite how macro engines work. - public IEnumerable SupportedExtensions - { - get { return Enumerable.Empty(); } - } - - //NOTE: We do not return any supported extensions because we don't want the MacroEngineFactory to return this - // macro engine when searching for engines via extension. Those types of engines are reserved for files that are - // stored in the ~/macroScripts folder and each engine must support unique extensions. This is a total Hack until - // we rewrite how macro engines work. - public IEnumerable SupportedUIExtensions - { - get { return Enumerable.Empty(); } - } - public bool Validate(string code, string tempFileName, IPublishedContent currentPage, out string errorMessage) { var temp = GetVirtualPathFromPhysicalPath(tempFileName); @@ -87,23 +63,23 @@ namespace Umbraco.Web.Macros errorMessage = string.Empty; return true; } - - public string Execute(MacroModel macro, IPublishedContent content) + + public MacroContent Execute(MacroModel macro, IPublishedContent content) { - if (macro == null) throw new ArgumentNullException("macro"); - if (content == null) throw new ArgumentNullException("content"); + if (macro == null) throw new ArgumentNullException(nameof(macro)); + if (content == null) throw new ArgumentNullException(nameof(content)); if (macro.ScriptName.IsNullOrWhiteSpace()) throw new ArgumentException("The ScriptName property of the macro object cannot be null or empty"); - + var http = _getHttpContext(); var umbCtx = _getUmbracoContext(); var routeVals = new RouteData(); routeVals.Values.Add("controller", "PartialViewMacro"); routeVals.Values.Add("action", "Index"); - routeVals.DataTokens.Add(Umbraco.Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage + routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage //lets render this controller as a child action - var viewContext = new ViewContext {ViewData = new ViewDataDictionary()};; - //try and extract the current view context from the route values, this would be set in the UmbracoViewPage or in + var viewContext = new ViewContext { ViewData = new ViewDataDictionary() }; + //try and extract the current view context from the route values, this would be set in the UmbracoViewPage or in // the UmbracoPageResult if POSTing to an MVC controller but rendering in Webforms if (http.Request.RequestContext.RouteData.DataTokens.ContainsKey(Mvc.Constants.DataTokenCurrentViewContext)) { @@ -116,7 +92,7 @@ namespace Umbraco.Web.Macros using (var controller = new PartialViewMacroController(macro, content)) { controller.ViewData = viewContext.ViewData; - + controller.ControllerContext = new ControllerContext(request, controller); //call the action to render @@ -124,12 +100,12 @@ namespace Umbraco.Web.Macros output = controller.RenderViewResultAsString(result); } - return output; + return new MacroContent { Text = output }; } private string GetVirtualPathFromPhysicalPath(string physicalPath) { - string rootpath = _getHttpContext().Server.MapPath("~/"); + var rootpath = _getHttpContext().Server.MapPath("~/"); physicalPath = physicalPath.Replace(rootpath, ""); physicalPath = physicalPath.Replace("\\", "/"); return "~/" + physicalPath; diff --git a/src/Umbraco.Web/Macros/UserControlMacroEngine.cs b/src/Umbraco.Web/Macros/UserControlMacroEngine.cs new file mode 100644 index 0000000000..38fb7d4d46 --- /dev/null +++ b/src/Umbraco.Web/Macros/UserControlMacroEngine.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using System.Web; +using System.Web.UI; +using umbraco; +using umbraco.cms.businesslogic.macro; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Macros +{ + class UserControlMacroEngine + { + public MacroContent Execute(MacroModel model) + { + var filename = model.TypeName; + + // ensure the file exists + var path = IOHelper.FindFile(filename); + if (File.Exists(IOHelper.MapPath(path)) == false) + throw new UmbracoException($"Failed to load control, file {filename} does not exist."); + + // load the control + var control = (UserControl)new UserControl().LoadControl(path); + control.ID = string.IsNullOrEmpty(model.MacroControlIdentifier) + ? GetControlUniqueId(filename) + : model.MacroControlIdentifier; + + // initialize the control + LogHelper.Info($"Loaded control \"{filename}\" with ID \"{control.ID}\"."); + //SetControlCurrentNode(control); // fixme what are the consequences? + UpdateControlProperties(control, model); + + return new MacroContent { Control = control }; + } + + // no more INode in v8 + /* + // sets the control CurrentNode|currentNode property + private static void SetControlCurrentNode(Control control) + { + var node = GetCurrentNode(); // get the current INode + SetControlCurrentNode(control, "CurrentNode", node); + SetControlCurrentNode(control, "currentNode", node); + } + + // sets the control 'propertyName' property, of type INode + private static void SetControlCurrentNode(Control control, string propertyName, INode node) + { + var currentNodeProperty = control.GetType().GetProperty(propertyName); + if (currentNodeProperty != null && currentNodeProperty.CanWrite && + currentNodeProperty.PropertyType.IsAssignableFrom(typeof(INode))) + { + currentNodeProperty.SetValue(control, node, null); + } + } + */ + + private static string GetControlUniqueId(string filename) + { + const string key = "MacroControlUniqueId"; + + var x = 0; + + if (HttpContext.Current != null) + { + if (HttpContext.Current.Items.Contains(key)) + x = (int)HttpContext.Current.Items[key]; + x += 1; + HttpContext.Current.Items[key] = x; + } + + return $"{Path.GetFileNameWithoutExtension(filename)}_{x}"; + } + + // set the control properties according to the model properties ie parameters + private static void UpdateControlProperties(Control control, MacroModel model) + { + var type = control.GetType(); + + foreach (var modelProperty in model.Properties) + { + var controlProperty = type.GetProperty(modelProperty.Key); + if (controlProperty == null) + { + LogHelper.Warn($"Control property \"{modelProperty.Key}\" doesn't exist or isn't accessible, skip."); + continue; + } + + var tryConvert = modelProperty.Value.TryConvertTo(controlProperty.PropertyType); + if (tryConvert.Success) + { + try + { + controlProperty.SetValue(control, tryConvert.Result, null); + LogHelper.Debug($"Set property \"{modelProperty.Key}\" value \"{modelProperty.Value}\"."); + } + catch (Exception e) + { + LogHelper.WarnWithException($"Failed to set property \"{modelProperty.Key}\" value \"{modelProperty.Value}\".", e); + } + } + else + { + LogHelper.Warn($"Failed to set property \"{modelProperty.Key}\" value \"{modelProperty.Value}\"."); + } + } + } + } +} diff --git a/src/Umbraco.Web/Macros/XsltMacroEngine.cs b/src/Umbraco.Web/Macros/XsltMacroEngine.cs new file mode 100644 index 0000000000..8377c22340 --- /dev/null +++ b/src/Umbraco.Web/Macros/XsltMacroEngine.cs @@ -0,0 +1,688 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using System.Web.Caching; +using System.Xml; +using System.Xml.XPath; +using System.Xml.Xsl; +using umbraco; +using umbraco.cms.businesslogic.macro; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Macros; +using Umbraco.Core.Xml.XPath; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.Macros +{ + /// + /// A macro engine using XSLT to execute. + /// + public class XsltMacroEngine + { + private readonly Func _getHttpContext; + private readonly Func _getUmbracoContext; + private readonly ProfilingLogger _plogger; + + public XsltMacroEngine(ProfilingLogger plogger) + { + _plogger = plogger; + + _getHttpContext = () => + { + if (HttpContext.Current == null) + throw new InvalidOperationException("The Xslt Macro Engine cannot execute with a null HttpContext.Current reference"); + return new HttpContextWrapper(HttpContext.Current); + }; + + _getUmbracoContext = () => + { + if (UmbracoContext.Current == null) + throw new InvalidOperationException("The Xslt Macro Engine cannot execute with a null UmbracoContext.Current reference"); + return UmbracoContext.Current; + }; + } + + #region Execute Xslt + + // executes the macro, relying on GetXsltTransform + // will pick XmlDocument or Navigator mode depending on the capabilities of the published caches + public MacroContent Execute(MacroModel model) + { + var hasCode = string.IsNullOrWhiteSpace(model.ScriptCode) == false; + var hasXslt = string.IsNullOrWhiteSpace(model.Xslt) == false; + + if (hasXslt == false && hasCode == false) + { + LogHelper.Warn("Xslt is empty"); + return MacroContent.Empty; + } + + if (hasCode && model.ScriptLanguage.InvariantEquals("xslt") == false) + { + LogHelper.Warn("Unsupported script language \"" + model.ScriptLanguage + "\"."); + return MacroContent.Empty; + } + + var msg = "Executing Xslt: " + (hasCode ? "Inline." : "Xslt=\"" + model.Xslt + "\"."); + using (_plogger.DebugDuration(msg, "ExecutedXslt.")) + { + // need these two here for error reporting + MacroNavigator macroNavigator = null; + XmlDocument macroXml = null; + + IXPathNavigable macroNavigable; + IXPathNavigable contentNavigable; + + var xmlCache = UmbracoContext.Current.ContentCache as PublishedCache.XmlPublishedCache.PublishedContentCache; + + if (xmlCache == null) + { + // a different cache + // inheriting from NavigableNavigator is required + + var contentNavigator = UmbracoContext.Current.ContentCache.CreateNavigator() as NavigableNavigator; + var mediaNavigator = UmbracoContext.Current.MediaCache.CreateNavigator() as NavigableNavigator; + + if (contentNavigator == null || mediaNavigator == null) + throw new Exception("Published caches XPathNavigator do not inherit from NavigableNavigator."); + + var parameters = new List(); + foreach (var prop in model.Properties) + AddMacroParameter(parameters, contentNavigator, mediaNavigator, prop.Key, prop.Type, prop.Value); + + macroNavigable = macroNavigator = new MacroNavigator(parameters); + contentNavigable = UmbracoContext.Current.ContentCache; + } + else + { + // the original XML cache + // render the macro on top of the Xml document + + var umbracoXml = xmlCache.GetXml(UmbracoContext.Current.InPreviewMode); + + macroXml = new XmlDocument(); + macroXml.LoadXml(""); + + foreach (var prop in model.Properties) + AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); + + macroNavigable = macroXml; + contentNavigable = umbracoXml; + } + + // this is somewhat ugly but eh... + var httpContext = _getHttpContext(); + if (httpContext.Request.QueryString["umbDebug"] != null && GlobalSettings.DebugMode) + { + var outerXml = macroXml?.OuterXml ?? macroNavigator.OuterXml; + var text = $"
Debug from {model.Name}
{HttpUtility.HtmlEncode(outerXml)}
"; + return new MacroContent { Text = text }; + } + + // get the transform + XslCompiledTransform transform; + if (hasCode) + { + try + { + using (var sreader = new StringReader(model.ScriptCode)) + using (var xreader = new XmlTextReader(sreader)) + { + transform = GetXsltTransform(xreader, GlobalSettings.DebugMode); + } + } + catch (Exception e) + { + throw new Exception("Failed to parse inline Xslt.", e); + } + } + else + { + try + { + transform = GetCachedXsltTransform(model.Xslt); + } + catch (Exception e) + { + throw new Exception($"Failed to read Xslt file \"{model.Xslt}\".", e); + } + } + + using (_plogger.DebugDuration("Performing transformation.", "Performed transformation.")) + { + try + { + var transformed = XsltTransform(_plogger, macroNavigable, contentNavigable, transform); + var text = TemplateUtilities.ResolveUrlsFromTextString(transformed); + return new MacroContent { Text = text }; + } + catch (Exception e) + { + throw new Exception($"Failed to exec Xslt file \"{model.Xslt}\".", e); + } + } + } + } + + /// + /// Adds the XSLT extension namespaces to the XSLT header using + /// {0} as the container for the namespace references and + /// {1} as the container for the exclude-result-prefixes + /// + /// The XSLT + /// The XSLT with {0} and {1} replaced. + /// This is done here because it needs the engine's XSLT extensions. + public static string AddXsltExtensionsToHeader(string xslt) + { + var namespaceList = new StringBuilder(); + var namespaceDeclaractions = new StringBuilder(); + foreach (var extension in GetXsltExtensions()) + { + namespaceList.Append(extension.Key).Append(' '); + namespaceDeclaractions.AppendFormat("xmlns:{0}=\"urn:{0}\" ", extension.Key); + } + + // parse xslt + xslt = xslt.Replace("{0}", namespaceDeclaractions.ToString()); + xslt = xslt.Replace("{1}", namespaceList.ToString()); + return xslt; + } + + public static string TestXsltTransform(ProfilingLogger plogger, string xsltText, int currentPageId = -1) + { + IXPathNavigable macroNavigable; + IXPathNavigable contentNavigable; + + var xmlCache = UmbracoContext.Current.ContentCache as PublishedCache.XmlPublishedCache.PublishedContentCache; + + var xslParameters = new Dictionary(); + xslParameters["currentPage"] = UmbracoContext.Current.ContentCache + .CreateNavigator() + .Select(currentPageId > 0 ? ("//* [@id=" + currentPageId + "]") : "//* [@parentID=-1]"); + + if (xmlCache == null) + { + // a different cache + // inheriting from NavigableNavigator is required + + var contentNavigator = UmbracoContext.Current.ContentCache.CreateNavigator() as NavigableNavigator; + if (contentNavigator == null) + throw new Exception("Published caches XPathNavigator do not inherit from NavigableNavigator."); + + var parameters = new List(); + macroNavigable = new MacroNavigator(parameters); + contentNavigable = UmbracoContext.Current.ContentCache; + } + else + { + // the original XML cache + // render the macro on top of the Xml document + + var umbracoXml = xmlCache.GetXml(UmbracoContext.Current.InPreviewMode); + + var macroXml = new XmlDocument(); + macroXml.LoadXml(""); + + macroNavigable = macroXml; + contentNavigable = umbracoXml; + } + + // for a test, do not try...catch + // but let the exceptions be thrown + + XslCompiledTransform transform; + using (var reader = new XmlTextReader(new StringReader(xsltText))) + { + transform = GetXsltTransform(reader, true); + } + var transformed = XsltTransform(plogger, macroNavigable, contentNavigable, transform, xslParameters); + + return transformed; + } + + public static string ExecuteItemRenderer(ProfilingLogger plogger, XslCompiledTransform transform, string itemData) + { + IXPathNavigable macroNavigable; + IXPathNavigable contentNavigable; + + var xmlCache = UmbracoContext.Current.ContentCache as PublishedCache.XmlPublishedCache.PublishedContentCache; + + if (xmlCache == null) + { + // a different cache + // inheriting from NavigableNavigator is required + + var contentNavigator = UmbracoContext.Current.ContentCache.CreateNavigator() as NavigableNavigator; + var mediaNavigator = UmbracoContext.Current.MediaCache.CreateNavigator() as NavigableNavigator; + + if (contentNavigator == null || mediaNavigator == null) + throw new Exception("Published caches XPathNavigator do not inherit from NavigableNavigator."); + + var parameters = new List(); + + macroNavigable = new MacroNavigator(parameters); + contentNavigable = UmbracoContext.Current.ContentCache; + } + else + { + // the original XML cache + // render the macro on top of the Xml document + + var umbracoXml = xmlCache.GetXml(UmbracoContext.Current.InPreviewMode); + + var macroXml = new XmlDocument(); + macroXml.LoadXml(""); + + macroNavigable = macroXml; + contentNavigable = umbracoXml; + } + + var xslParameters = new Dictionary { { "itemData", itemData } }; + return XsltTransform(plogger, macroNavigable, contentNavigable, transform, xslParameters); + } + + #endregion + + #region XsltTransform + + // running on the XML cache, document mode + // add parameters to the root node + // note: contains legacy dirty code + private static void AddMacroXmlNode(XmlDocument umbracoXml, XmlDocument macroXml, + string macroPropertyAlias, string macroPropertyType, string macroPropertyValue) + { + var macroXmlNode = macroXml.CreateNode(XmlNodeType.Element, macroPropertyAlias, string.Empty); + + // if no value is passed, then use the current "pageID" as value + var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; + + LogHelper.Info($"Xslt node adding search start ({macroPropertyAlias},{macroPropertyValue})"); + + switch (macroPropertyType) + { + case "contentTree": + var nodeId = macroXml.CreateAttribute("nodeID"); + nodeId.Value = contentId; + macroXmlNode.Attributes.SetNamedItem(nodeId); + + // Get subs + try + { + macroXmlNode.AppendChild(macroXml.ImportNode(umbracoXml.GetElementById(contentId), true)); + } + catch + { } + break; + + case "contentCurrent": + var importNode = macroPropertyValue == string.Empty + ? umbracoXml.GetElementById(contentId) + : umbracoXml.GetElementById(macroPropertyValue); + + var currentNode = macroXml.ImportNode(importNode, true); + + // remove all sub content nodes + foreach (XmlNode n in currentNode.SelectNodes("*[@isDoc]")) + currentNode.RemoveChild(n); + + macroXmlNode.AppendChild(currentNode); + + break; + + case "contentSubs": // disable that one, it does not work anyway... + //x.LoadXml(""); + //x.FirstChild.AppendChild(x.ImportNode(umbracoXml.GetElementById(contentId), true)); + //macroXmlNode.InnerXml = TransformMacroXml(x, "macroGetSubs.xsl"); + break; + + case "contentAll": + macroXmlNode.AppendChild(macroXml.ImportNode(umbracoXml.DocumentElement, true)); + break; + + case "contentRandom": + XmlNode source = umbracoXml.GetElementById(contentId); + if (source != null) + { + var sourceList = source.SelectNodes("*[@isDoc]"); + if (sourceList.Count > 0) + { + int rndNumber; + var r = library.GetRandom(); + lock (r) + { + rndNumber = r.Next(sourceList.Count); + } + var node = macroXml.ImportNode(sourceList[rndNumber], true); + // remove all sub content nodes + foreach (XmlNode n in node.SelectNodes("*[@isDoc]")) + node.RemoveChild(n); + + macroXmlNode.AppendChild(node); + } + else + LogHelper.Warn("Error adding random node - parent (" + macroPropertyValue + ") doesn't have children!"); + } + else + LogHelper.Warn("Error adding random node - parent (" + macroPropertyValue + ") doesn't exists!"); + break; + + case "mediaCurrent": + if (string.IsNullOrEmpty(macroPropertyValue) == false) + { + //var c = new global::umbraco.cms.businesslogic.Content(int.Parse(macroPropertyValue)); + //macroXmlNode.AppendChild(macroXml.ImportNode(c.ToXml(global::umbraco.content.Instance.XmlContent, false), true)); + var nav = UmbracoContext.Current.MediaCache.CreateNodeNavigator(int.Parse(macroPropertyValue), false); + if (nav != null) + macroXmlNode.AppendChild(macroXml.ReadNode(nav.ReadSubtree())); + } + break; + + default: + macroXmlNode.InnerText = HttpUtility.HtmlDecode(macroPropertyValue); + break; + } + macroXml.FirstChild.AppendChild(macroXmlNode); + } + + // running on a navigable cache, navigable mode + // add parameters to the macro parameters collection + private static void AddMacroParameter(ICollection parameters, + NavigableNavigator contentNavigator, NavigableNavigator mediaNavigator, + string macroPropertyAlias, string macroPropertyType, string macroPropertyValue) + { + // if no value is passed, then use the current "pageID" as value + var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; + + LogHelper.Info($"Xslt node adding search start ({macroPropertyAlias},{macroPropertyValue})"); + + // beware! do not use the raw content- or media- navigators, but clones !! + + switch (macroPropertyType) + { + case "contentTree": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + attributes: new Dictionary { { "nodeID", contentId } })); + + break; + + case "contentPicker": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + case "contentSubs": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 1)); + break; + + case "contentAll": + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, contentNavigator.Clone())); + break; + + case "contentRandom": + var nav = contentNavigator.Clone(); + if (nav.MoveToId(contentId)) + { + var descendantIterator = nav.Select("./* [@isDoc]"); + if (descendantIterator.MoveNext()) + { + // not empty - and won't change + var descendantCount = descendantIterator.Count; + + int index; + var r = library.GetRandom(); + lock (r) + { + index = r.Next(descendantCount); + } + + while (index > 0 && descendantIterator.MoveNext()) + index--; + + var node = descendantIterator.Current.UnderlyingObject as INavigableContent; + if (node != null) + { + nav = contentNavigator.CloneWithNewRoot(node.Id); + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); + } + else + throw new InvalidOperationException("Iterator contains non-INavigableContent elements."); + } + else + LogHelper.Warn("Error adding random node - parent (" + macroPropertyValue + ") doesn't have children!"); + } + else + LogHelper.Warn("Error adding random node - parent (" + macroPropertyValue + ") doesn't exists!"); + break; + + case "mediaCurrent": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + mediaNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + default: + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, HttpUtility.HtmlDecode(macroPropertyValue))); + break; + } + } + + // gets the result of the xslt transform + private static string XsltTransform(ProfilingLogger plogger, IXPathNavigable macroNavigable, IXPathNavigable contentNavigable, + XslCompiledTransform xslt, IDictionary xslParameters = null) + { + TextWriter tw = new StringWriter(); + + XsltArgumentList xslArgs; + using (plogger.DebugDuration("Adding Xslt extensions", "Added Xslt extensions")) + { + xslArgs = GetXsltArgumentListWithExtensions(); + var lib = new library(); + xslArgs.AddExtensionObject("urn:umbraco.library", lib); + } + + // add parameters + if (xslParameters == null || xslParameters.ContainsKey("currentPage") == false) + { + // note: "PageId" is a legacy stuff that might be != from what's in current PublishedContentRequest + var currentPageId = UmbracoContext.Current.PageId; + var current = contentNavigable.CreateNavigator().Select("//* [@id=" + currentPageId + "]"); + xslArgs.AddParam("currentPage", string.Empty, current); + } + if (xslParameters != null) + foreach (var parameter in xslParameters) + xslArgs.AddParam(parameter.Key, string.Empty, parameter.Value); + + // transform + using (plogger.DebugDuration("Executing Xslt transform", "Executed Xslt transform")) + { + xslt.Transform(macroNavigable, xslArgs, tw); + } + + return tw.ToString(); + } + + #endregion + + #region Manage transforms + + private static XslCompiledTransform GetCachedXsltTransform(string filename) + { + //TODO: SD: Do we really need to cache this?? + var filepath = IOHelper.MapPath(SystemDirectories.Xslt.EnsureEndsWith('/') + filename); + return ApplicationContext.Current.ApplicationCache.GetCacheItem( + CacheKeys.MacroXsltCacheKey + filename, + CacheItemPriority.Default, + new CacheDependency(filepath), + () => + { + using (var xslReader = new XmlTextReader(filepath)) + { + return GetXsltTransform(xslReader, GlobalSettings.DebugMode); + } + }); + } + + public static XslCompiledTransform GetXsltTransform(XmlTextReader xslReader, bool debugMode) + { + var transform = new XslCompiledTransform(debugMode); + var xslResolver = new XmlUrlResolver + { + Credentials = CredentialCache.DefaultCredentials + }; + + xslReader.EntityHandling = EntityHandling.ExpandEntities; + xslReader.DtdProcessing = DtdProcessing.Parse; + + try + { + transform.Load(xslReader, XsltSettings.TrustedXslt, xslResolver); + } + finally + { + xslReader.Close(); + } + + return transform; + } + + #endregion + + #region Manage extensions + + /* + private static readonly string XsltExtensionsConfig = + IOHelper.MapPath(SystemDirectories.Config + "/xsltExtensions.config"); + + private static readonly Func XsltExtensionsDependency = + () => new CacheDependency(XsltExtensionsConfig); + */ + + // creates and return an Xslt argument list with all Xslt extensions. + public static XsltArgumentList GetXsltArgumentListWithExtensions() + { + var xslArgs = new XsltArgumentList(); + + foreach (var extension in GetXsltExtensions()) + { + var extensionNamespace = "urn:" + extension.Key; + xslArgs.AddExtensionObject(extensionNamespace, extension.Value); + LogHelper.Info($"Extension added: {extensionNamespace}, {extension.Value.GetType().Name}"); + } + + return xslArgs; + } + + /* + // gets the collection of all XSLT extensions for macros + // ie predefined, configured in the config file, and marked with the attribute + public static Dictionary GetCachedXsltExtensions() + { + // We could cache the extensions in a static variable but then the cache + // would not be refreshed when the .config file is modified. An application + // restart would be required. Better use the cache and add a dependency. + + // SD: The only reason the above statement might be true is because the xslt extension .config file is not a + // real config file!! if it was, we wouldn't have this issue. Having these in a static variable would be preferred! + // If you modify a config file, the app restarts and thus all static variables are reset. + // Having this stuff in cache just adds to the gigantic amount of cache data and will cause more cache turnover to happen. + + return ApplicationContext.Current.ApplicationCache.GetCacheItem( + "UmbracoXsltExtensions", + CacheItemPriority.NotRemovable, // NH 4.7.1, Changing to NotRemovable + null, // no refresh action + XsltExtensionsDependency(), // depends on the .config file + TimeSpan.FromDays(1), // expires in 1 day (?) + GetXsltExtensions); + } + */ + + // actually gets the collection of all XSLT extensions for macros + // ie predefined, configured in the config file, and marked with the attribute + public static Dictionary GetXsltExtensions() + { + return XsltExtensionsResolver.Current.XsltExtensions + .ToDictionary(x => x.Namespace, x => x.ExtensionObject); + + /* + // initialize the collection + // there is no "predefined" extensions anymore + var extensions = new Dictionary(); + + // Load the XSLT extensions configuration + var xsltExt = new XmlDocument(); + xsltExt.Load(XsltExtensionsConfig); + + // get the configured types + var extensionsNode = xsltExt.SelectSingleNode("/XsltExtensions"); + if (extensionsNode != null) + foreach (var attributes in extensionsNode.Cast() + .Where(x => x.NodeType == XmlNodeType.Element) + .Select(x => x.Attributes)) + { + Debug.Assert(attributes["assembly"] != null, "Extension attribute 'assembly' not specified."); + Debug.Assert(attributes["type"] != null, "Extension attribute 'type' not specified."); + Debug.Assert(attributes["alias"] != null, "Extension attribute 'alias' not specified."); + + // load the extension assembly + var extensionFile = IOHelper.MapPath(string.Format("{0}/{1}.dll", + SystemDirectories.Bin, attributes["assembly"].Value)); + + Assembly extensionAssembly; + try + { + extensionAssembly = Assembly.LoadFrom(extensionFile); + } + catch (Exception ex) + { + throw new Exception( + String.Format("Could not load assembly {0} for XSLT extension {1}. Please check config/xsltExtensions.config.", + extensionFile, attributes["alias"].Value), ex); + } + + // load the extension type + var extensionType = extensionAssembly.GetType(attributes["type"].Value); + if (extensionType == null) + throw new Exception( + String.Format("Could not load type {0} ({1}) for XSLT extension {2}. Please check config/xsltExtensions.config.", + attributes["type"].Value, extensionFile, attributes["alias"].Value)); + + // create an instance and add it to the extensions list + extensions.Add(attributes["alias"].Value, Activator.CreateInstance(extensionType)); + } + + // get types marked with XsltExtension attribute + var foundExtensions = PluginManager.Current.ResolveXsltExtensions(); + foreach (var xsltType in foundExtensions) + { + var attributes = xsltType.GetCustomAttributes(true); + var xsltTypeName = xsltType.FullName; + foreach (var ns in attributes + .Select(attribute => string.IsNullOrEmpty(attribute.Namespace) ? attribute.Namespace : xsltTypeName)) + { + extensions.Add(ns, Activator.CreateInstance(xsltType)); + } + } + + return extensions; + */ + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 7593644285..b8ce099122 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -6,6 +6,7 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Routing; +using Domain = Umbraco.Web.Routing.Domain; namespace Umbraco.Web.Models { @@ -48,8 +49,11 @@ namespace Umbraco.Web.Models ? null // for tests only : umbracoContext.ContentCache.GetRouteById(contentId); // cached - var domainHelper = new DomainHelper(domainService); - IDomain domain; + var domainCache = umbracoContext == null + ? new PublishedCache.XmlPublishedCache.DomainCache(domainService) // for tests only + : umbracoContext.Facade.DomainCache; // default + var domainHelper = new DomainHelper(domainCache); + Domain domain; if (route == null) { @@ -67,7 +71,7 @@ namespace Umbraco.Web.Models hasDomain = content != null && domainHelper.NodeHasDomains(content.Id); } - domain = hasDomain ? domainHelper.DomainForNode(content.Id, current).UmbracoDomain : null; + domain = hasDomain ? domainHelper.DomainForNode(content.Id, current) : null; } else { @@ -77,14 +81,14 @@ namespace Umbraco.Web.Models var pos = route.IndexOf('/'); domain = pos == 0 ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current); } - var rootContentId = domain == null ? -1 : domain.RootContentId; - var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, rootContentId); + var rootContentId = domain == null ? -1 : domain.ContentId; + var wcDomain = DomainHelper.FindWildcardDomainInPath(domainCache.GetAll(true), contentPath, rootContentId); - if (wcDomain != null) return new CultureInfo(wcDomain.LanguageIsoCode); - if (domain != null) return new CultureInfo(domain.LanguageIsoCode); + if (wcDomain != null) return wcDomain.Culture; + if (domain != null) return domain.Culture; return GetDefaultCulture(localizationService); } diff --git a/src/Umbraco.Web/Models/PublishedProperty.cs b/src/Umbraco.Web/Models/PublishedProperty.cs index 59618c047e..5aa656c5eb 100644 --- a/src/Umbraco.Web/Models/PublishedProperty.cs +++ b/src/Umbraco.Web/Models/PublishedProperty.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Models { if (propertyType.IsDetachedOrNested == false) throw new ArgumentException("Property type is neither detached nor nested.", "propertyType"); - var property = UmbracoContext.Current.ContentCache.InnerCache.CreateDetachedProperty(propertyType, value, isPreviewing); + var property = UmbracoContext.Current.ContentCache.CreateDetachedProperty(propertyType, value, isPreviewing); return property; } diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs deleted file mode 100644 index b5c3d850d2..0000000000 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Xml.XPath; -using Umbraco.Core.Models; -using Umbraco.Core.Xml; - -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides access to cached contents in a specified context. - /// - public abstract class ContextualPublishedCache - { - protected readonly UmbracoContext UmbracoContext; - - /// - /// Initializes a new instance of the with a context. - /// - /// The context. - protected ContextualPublishedCache(UmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - } - - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on context. - public IPublishedContent GetById(int contentId) - { - return GetById(UmbracoContext.InPreviewMode, contentId); - } - - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - public abstract IPublishedContent GetById(bool preview, int contentId); - - /// - /// Gets content at root. - /// - /// The contents. - /// Considers published or unpublished content depending on context. - public IEnumerable GetAtRoot() - { - return GetAtRoot(UmbracoContext.InPreviewMode); - } - - /// - /// Gets contents at root. - /// - /// A value indicating whether to consider unpublished content. - /// The contents. - public abstract IEnumerable GetAtRoot(bool preview); - - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// Considers published or unpublished content depending on context. - /// - public IPublishedContent GetSingleByXPath(string xpath, params XPathVariable[] vars) - { - return GetSingleByXPath(UmbracoContext.InPreviewMode, xpath, vars); - } - - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// Considers published or unpublished content depending on context. - /// - public IPublishedContent GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars) - { - return GetSingleByXPath(UmbracoContext.InPreviewMode, xpath, vars); - } - - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); - - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public abstract IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - - /// - /// Gets content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// Considers published or unpublished content depending on context. - /// - public IEnumerable GetByXPath(string xpath, params XPathVariable[] vars) - { - return GetByXPath(UmbracoContext.InPreviewMode, xpath, vars); - } - - /// - /// Gets content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// Considers published or unpublished content depending on context. - /// - public IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars) - { - return GetByXPath(UmbracoContext.InPreviewMode, xpath, vars); - } - - /// - /// Gets content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public abstract IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); - - /// - /// Gets content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - - /// - /// Gets an XPath navigator that can be used to navigate content. - /// - /// The XPath navigator. - /// Considers published or unpublished content depending on context. - public XPathNavigator GetXPathNavigator() - { - return GetXPathNavigator(UmbracoContext.InPreviewMode); - } - - /// - /// Gets an XPath navigator that can be used to navigate content. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath navigator. - public abstract XPathNavigator GetXPathNavigator(bool preview); - - /// - /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator - /// and that navigator is a NavigableNavigator. - /// - public abstract bool XPathNavigatorIsNavigable { get; } - - /// - /// Gets a value indicating whether the underlying non-contextual cache contains content. - /// - /// A value indicating whether the underlying non-contextual cache contains content. - /// Considers published or unpublished content depending on context. - public bool HasContent() - { - return HasContent(UmbracoContext.InPreviewMode); - } - - /// - /// Gets a value indicating whether the underlying non-contextual cache contains content. - /// - /// A value indicating whether to consider unpublished content. - /// A value indicating whether the underlying non-contextual cache contains content. - public abstract bool HasContent(bool preview); - } -} diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs deleted file mode 100644 index 6e897c2c2e..0000000000 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Xml.XPath; -using Umbraco.Core.Models; -using Umbraco.Core.Xml; - -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides access to cached contents in a specified context. - /// - /// The type of the underlying published cache. - /// The type differenciates between the content cache and the media cache, - /// ie it will be either IPublishedContentCache or IPublishedMediaCache. - public abstract class ContextualPublishedCache : ContextualPublishedCache - where T : IPublishedCache - { - private readonly T _cache; - - /// - /// Initializes a new instance of the with a context and a published cache. - /// - /// The context. - /// The cache. - protected ContextualPublishedCache(UmbracoContext umbracoContext, T cache) - : base(umbracoContext) - { - _cache = cache; - } - - /// - /// Gets the underlying published cache. - /// - public T InnerCache { get { return _cache; } } - - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - public override IPublishedContent GetById(bool preview, int contentId) - { - return _cache.GetById(UmbracoContext, preview, contentId); - } - - /// - /// Gets content at root. - /// - /// A value indicating whether to consider unpublished content. - /// The contents. - public override IEnumerable GetAtRoot(bool preview) - { - return _cache.GetAtRoot(UmbracoContext, preview); - } - - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public override IPublishedContent GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars) - { - return _cache.GetSingleByXPath(UmbracoContext, preview, xpath, vars); - } - - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars) - { - return _cache.GetSingleByXPath(UmbracoContext, preview, xpath, vars); - } - - /// - /// Gets content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public override IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars) - { - return _cache.GetByXPath(UmbracoContext, preview, xpath, vars); - } - - /// - /// Gets content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public override IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars) - { - return _cache.GetByXPath(UmbracoContext, preview, xpath, vars); - } - - /// - /// Gets an XPath navigator that can be used to navigate content. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath navigator. - public override XPathNavigator GetXPathNavigator(bool preview) - { - return _cache.GetXPathNavigator(UmbracoContext, preview); - } - - /// - /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator - /// and that navigator is a NavigableNavigator. - /// - public override bool XPathNavigatorIsNavigable { get { return _cache.XPathNavigatorIsNavigable; } } - - /// - /// Gets a value indicating whether the underlying non-contextual cache contains content. - /// - /// A value indicating whether to consider unpublished content. - /// A value indicating whether the underlying non-contextual cache contains content. - public override bool HasContent(bool preview) - { - return _cache.HasContent(UmbracoContext, preview); - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs deleted file mode 100644 index ebe2743f4b..0000000000 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Umbraco.Core.Models; - -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides access to cached documents in a specified context. - /// - public class ContextualPublishedContentCache : ContextualPublishedCache - { - /// - /// Initializes a new instance of the class with a published content cache and a context. - /// - /// A published content cache. - /// A context. - internal ContextualPublishedContentCache(IPublishedContentCache cache, UmbracoContext umbracoContext) - : base(umbracoContext, cache) - { } - - /// - /// Gets content identified by a route. - /// - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - /// Considers published or unpublished content depending on context. - /// - public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null) - { - return GetByRoute(UmbracoContext.InPreviewMode, route, hideTopLevelNode); - } - - /// - /// Gets content identified by a route. - /// - /// A value indicating whether to consider unpublished content. - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null) - { - return InnerCache.GetByRoute(UmbracoContext, preview, route, hideTopLevelNode); - } - - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The route. - /// Considers published or unpublished content depending on context. - public string GetRouteById(int contentId) - { - return GetRouteById(UmbracoContext.InPreviewMode, contentId); - } - - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The route. - /// Considers published or unpublished content depending on context. - public string GetRouteById(bool preview, int contentId) - { - return InnerCache.GetRouteById(UmbracoContext, preview, contentId); - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs deleted file mode 100644 index 494a0cd419..0000000000 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Umbraco.Core.Models; - -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides access to cached medias in a specified context. - /// - public class ContextualPublishedMediaCache : ContextualPublishedCache - { - /// - /// Initializes a new instance of the class with a published media cache and a context. - /// - /// A published media cache. - /// A context. - internal ContextualPublishedMediaCache(IPublishedMediaCache cache, UmbracoContext umbracoContext) - : base(umbracoContext, cache) - { } - } -} diff --git a/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs b/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs new file mode 100644 index 0000000000..accc3e0f05 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs @@ -0,0 +1,47 @@ +using System; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Models.Membership; +using Umbraco.Web.Cache; + +namespace Umbraco.Web.PublishedCache +{ + abstract class FacadeServiceBase : IFacadeService + { + // FIXME need a facade accessor of some sort? to init the facade service! of course! + private Func _getFacadeFunc = () => UmbracoContext.Current == null ? null : UmbracoContext.Current.Facade; + + public Func GetFacadeFunc + { + get { return _getFacadeFunc; } + set + { + using (Resolution.Configuration) + { + _getFacadeFunc = value; + } + } + } + + public abstract IFacade CreateFacade(string previewToken); + + public IFacade GetFacade() + { + var caches = _getFacadeFunc(); + if (caches == null) + throw new Exception("Carrier's caches is null."); + return caches; + } + + public abstract string EnterPreview(IUser user, int contentId); + public abstract void RefreshPreview(string previewToken, int contentId); + public abstract void ExitPreview(string previewToken); + public abstract void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); + public abstract void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); + public abstract void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); + public abstract void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); + public abstract void Notify(DomainCacheRefresher.JsonPayload[] payloads); + + public virtual void Dispose() + { } + } +} diff --git a/src/Umbraco.Web/PublishedCache/FacadeServiceResolver.cs b/src/Umbraco.Web/PublishedCache/FacadeServiceResolver.cs new file mode 100644 index 0000000000..776f33e94a --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/FacadeServiceResolver.cs @@ -0,0 +1,17 @@ +using LightInject; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.PublishedCache +{ + // resolves the IFacadeService + // from the IServiceContainer + // best to inject the service - this is for when it's not possible + public class FacadeServiceResolver : ContainerSingleObjectResolver + { + public FacadeServiceResolver(IServiceContainer container) + : base(container) + { } + + public IFacadeService Service => Value; + } +} diff --git a/src/Umbraco.Web/PublishedCache/IDomainCache.cs b/src/Umbraco.Web/PublishedCache/IDomainCache.cs new file mode 100644 index 0000000000..113533f500 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/IDomainCache.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.PublishedCache +{ + public interface IDomainCache + { + IEnumerable GetAll(bool includeWildcards); + IEnumerable GetAssigned(int contentId, bool includeWildcards); + } +} diff --git a/src/Umbraco.Web/PublishedCache/IFacade.cs b/src/Umbraco.Web/PublishedCache/IFacade.cs new file mode 100644 index 0000000000..8c68e88fe8 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/IFacade.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Web.PublishedCache +{ + /// + /// The Umbraco facade. + /// + public interface IFacade + { + /// + /// Gets the . + /// + IPublishedContentCache ContentCache { get; } + + /// + /// Gets the . + /// + IPublishedMediaCache MediaCache { get; } + + /// + /// Gets the . + /// + IPublishedMemberCache MemberCache { get; } + + /// + /// Gets the . + /// + IDomainCache DomainCache { get; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/IFacadeService.cs b/src/Umbraco.Web/PublishedCache/IFacadeService.cs new file mode 100644 index 0000000000..5cdcbc6182 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/IFacadeService.cs @@ -0,0 +1,147 @@ +using System; +using Umbraco.Core.Models.Membership; +using Umbraco.Web.Cache; + +namespace Umbraco.Web.PublishedCache +{ + /// + /// Creates and manages IFacade. + /// + public interface IFacadeService : IDisposable + { + #region PublishedCaches + + /* Various places (such as Node) want to access the XML content, today as an XmlDocument + * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need + * to find out how to get that navigator. + * + * Because a cache such as the DrippingCache is contextual ie it has a "snapshot" nothing + * and remains consistent over the snapshot, the navigator should come from the "current" + * snapshot. + * + * The service creates those snapshots in IPublishedCaches objects. + * + * Places such as Node need to be able to find the "current" one so the factory has a + * notion of what is "current". In most cases, the IPublishedCaches object is created + * and registered against an UmbracoContext, and that context is then used as "current". + * + * But for tests we need to have a way to specify what's the "current" object & preview. + * Which is defined in PublishedCachesServiceBase. + * + */ + + /// + /// Creates a facade. + /// + /// A preview token, or null if not previewing. + /// A facade. + IFacade CreateFacade(string previewToken); + + /// + /// Gets the current facade. + /// + /// The current facade. + /// + IFacade GetFacade(); + + #endregion + + #region Preview + + /* Later on we can imagine that EnterPreview would handle a "level" that would be either + * the content only, or the content's branch, or the whole tree + it could be possible + * to register filters against the factory to filter out which nodes should be preview + * vs non preview. + * + * EnterPreview() returns the previewToken. It is up to callers to store that token + * wherever they want, most probably in a cookie. + * + */ + + /// + /// Enters preview for specified user and content. + /// + /// The user. + /// The content identifier. + /// A preview token. + /// + /// Tells the caches that they should prepare any data that they would be keeping + /// in order to provide preview to a give user. In the Xml cache this means creating the Xml + /// file, though other caches may do things differently. + /// Does not handle the preview token storage (cookie, etc) that must be handled separately. + /// + string EnterPreview(IUser user, int contentId); + + /// + /// Refreshes preview for a specified content. + /// + /// The preview token. + /// The content identifier. + /// Tells the caches that they should update any data that they would be keeping + /// in order to provide preview to a given user. In the Xml cache this means updating the Xml + /// file, though other caches may do things differently. + void RefreshPreview(string previewToken, int contentId); + + /// + /// Exits preview for a specified preview token. + /// + /// The preview token. + /// + /// Tells the caches that they can dispose of any data that they would be keeping + /// in order to provide preview to a given user. In the Xml cache this means deleting the Xml file, + /// though other caches may do things differently. + /// Does not handle the preview token storage (cookie, etc) that must be handled separately. + /// + void ExitPreview(string previewToken); + + #endregion + + #region Changes + + /* An IPublishedCachesService implementation can rely on transaction-level events to update + * its internal, database-level data, as these events are purely internal. However, it cannot + * rely on cache refreshers CacheUpdated events to update itself, as these events are external + * and the order-of-execution of the handlers cannot be guaranteed, which means that some + * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers + * explicitely notify the service of changes. + * + */ + + /// + /// Notifies of content cache refresher changes. + /// + /// The changes. + /// A value indicating whether draft contents have been changed in the cache. + /// A value indicating whether published contents have been changed in the cache. + void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); + + /// + /// Notifies of media cache refresher changes. + /// + /// The changes. + /// A value indicating whether medias have been changed in the cache. + void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); + + // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. + + /// + /// Notifies of content type refresher changes. + /// + /// The changes. + void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); + + /// + /// Notifies of data type refresher changes. + /// + /// The changes. + void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); + + /// + /// Notifies of domain refresher changes. + /// + /// The changes. + void Notify(DomainCacheRefresher.JsonPayload[] payloads); + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 6cf77539e2..4f70b0eaa4 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -1,112 +1,198 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; using System.Xml.XPath; -using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; namespace Umbraco.Web.PublishedCache { /// /// Provides access to cached contents. - /// - public interface IPublishedCache + /// + public interface IPublishedCache : IXPathNavigable { /// /// Gets a content identified by its unique identifier. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The content unique identifier. /// The content, or null. - /// The value of overrides the context. - IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int contentId); + /// The value of overrides defaults. + IPublishedContent GetById(bool preview, int 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(int contentId); + + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// The value of overrides defaults. + bool HasById(bool preview, int contentId); + + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// Considers published or unpublished content depending on defaults. + bool HasById(int contentId); /// /// Gets contents at root. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The contents. - /// The value of overrides the context. - IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview); + /// The value of overrides defaults. + IEnumerable GetAtRoot(bool preview); + + /// + /// Gets contents at root. + /// + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetAtRoot(); /// /// Gets a content resulting from an XPath query. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. /// The content, or null. - /// The value of overrides the context. - IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars); + /// The value of overrides defaults. + IPublishedContent GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); + + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent GetSingleByXPath(string xpath, params XPathVariable[] vars); /// /// Gets a content resulting from an XPath query. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. /// The content, or null. - /// The value of overrides the context. - IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars); + /// The value of overrides defaults. + IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); + + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars); /// /// Gets contents resulting from an XPath query. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. /// The contents. - /// The value of overrides the context. - IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars); + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); + + /// + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(string xpath, params XPathVariable[] vars); /// /// Gets contents resulting from an XPath query. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. /// The contents. - /// The value of overrides the context. - IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars); + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); /// - /// Gets an XPath navigator that can be used to navigate contents. + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars); + + /// + /// Creates an XPath navigator that can be used to navigate contents. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The XPath navigator. - /// The value of overrides the context. - XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview); + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// + XPathNavigator CreateNavigator(bool preview); /// - /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator - /// and that navigator is a NavigableNavigator. + /// Creates an XPath navigator that can be used to navigate one node. /// - bool XPathNavigatorIsNavigable { get; } + /// The node identifier. + /// A value indicating whether to consider unpublished content. + /// The XPath navigator, or null. + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// Navigates over the node - and only the node, ie no children. Exists only for backward + /// compatibility + transition reasons, we should obsolete that one as soon as possible. + /// If the node does not exist, returns null. + /// + XPathNavigator CreateNodeNavigator(int id, bool preview); /// /// Gets a value indicating whether the cache contains published content. /// - /// The context. /// A value indicating whether to consider unpublished content. /// A value indicating whether the cache contains published content. - /// The value of overrides the context. - bool HasContent(UmbracoContext umbracoContext, bool preview); - - //TODO: SD: We should make this happen! This will allow us to natively do a GetByDocumentType query - // on the UmbracoHelper (or an internal DataContext that it uses, etc...) - // One issue is that we need to make media work as fast as we can and need to create a ConvertFromMediaObject - // method in the DefaultPublishedMediaStore, there's already a TODO noting this but in order to do that we'll - // have to also use Examine as much as we can so we don't have to make db calls for looking up things like the - // node type alias, etc... in order to populate the created IPublishedContent object. - //IEnumerable GetDocumentsByType(string docTypeAlias); + /// The value of overrides defaults. + bool HasContent(bool preview); + + /// + /// Gets a value indicating whether the cache contains published content. + /// + /// A value indicating whether the cache contains published content. + /// Considers published or unpublished content depending on defaults. + bool HasContent(); + + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + PublishedContentType GetContentType(int id); + + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + PublishedContentType GetContentType(string alias); + + // fixme - can we implement this, now? maybe only with NuCache else will throw NotImplemented... + IEnumerable GetByContentType(PublishedContentType contentType); } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCaches.cs b/src/Umbraco.Web/PublishedCache/IPublishedCaches.cs deleted file mode 100644 index 82609b7457..0000000000 --- a/src/Umbraco.Web/PublishedCache/IPublishedCaches.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides caches (content and media). - /// - /// Groups caches that _may_ be related. - interface IPublishedCaches - { - /// - /// Creates a contextual content cache for a specified context. - /// - /// The context. - /// A new contextual content cache for the specified context. - ContextualPublishedContentCache CreateContextualContentCache(UmbracoContext context); - - /// - /// Creates a contextual media cache for a specified context. - /// - /// The context. - /// A new contextual media cache for the specified context. - ContextualPublishedMediaCache CreateContextualMediaCache(UmbracoContext context); - } -} diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index b73e594727..c755665dff 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Models; namespace Umbraco.Web.PublishedCache { @@ -12,7 +8,6 @@ namespace Umbraco.Web.PublishedCache /// /// Gets content identified by a route. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The route /// A value forcing the HideTopLevelNode setting. @@ -20,19 +15,39 @@ namespace Umbraco.Web.PublishedCache /// /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. /// If is null then the settings value is used. - /// The value of overrides the context. + /// The value of overrides defaults. /// - IPublishedContent GetByRoute(UmbracoContext umbracoContext, bool preview, string route, bool? hideTopLevelNode = null); + IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null); + + /// + /// Gets content identified by a route. + /// + /// The route + /// A value forcing the HideTopLevelNode setting. + /// The content, or null. + /// + /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. + /// If is null then the settings value is used. + /// Considers published or unpublished content depending on defaults. + /// + IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null); /// /// Gets the route for a content identified by its unique identifier. /// - /// The context. /// A value indicating whether to consider unpublished content. /// The content unique identifier. /// The route. - /// The value of overrides the context. - string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId); + /// The value of overrides defaults. + string GetRouteById(bool preview, int contentId); + + /// + /// Gets the route for a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The route. + /// Considers published or unpublished content depending on defaults. + string GetRouteById(int contentId); /// /// Creates a detached property. diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs index 7a3e6500e8..c25867229a 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Umbraco.Web.PublishedCache +namespace Umbraco.Web.PublishedCache { public interface IPublishedMediaCache : IPublishedCache - { - } + { } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs new file mode 100644 index 0000000000..53d37a8d31 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs @@ -0,0 +1,35 @@ +using System.Xml.XPath; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.PublishedCache +{ + public interface IPublishedMemberCache : IXPathNavigable + { + IPublishedContent GetByProviderKey(object key); + IPublishedContent GetById(int memberId); + IPublishedContent GetByUsername(string username); + IPublishedContent GetByEmail(string email); + IPublishedContent GetByMember(IMember member); + + XPathNavigator CreateNavigator(bool preview); + + // if the node does not exist, return null + XPathNavigator CreateNodeNavigator(int id, bool preview); + + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + PublishedContentType GetContentType(int id); + + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + PublishedContentType GetContentType(string alias); + } +} diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs new file mode 100644 index 0000000000..b92b985bdf --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Xml.XPath; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Xml; +using Umbraco.Core.Models; + +namespace Umbraco.Web.PublishedCache +{ + abstract class PublishedCacheBase : IPublishedCache + { + public bool PreviewDefault { get; } + + protected PublishedCacheBase(bool previewDefault) + { + PreviewDefault = previewDefault; + } + + public abstract IPublishedContent GetById(bool preview, int contentId); + + public IPublishedContent GetById(int contentId) + { + return GetById(PreviewDefault, contentId); + } + + public abstract bool HasById(bool preview, int contentId); + + public bool HasById(int contentId) + { + return HasById(PreviewDefault, contentId); + } + + public abstract IEnumerable GetAtRoot(bool preview); + + public IEnumerable GetAtRoot() + { + return GetAtRoot(PreviewDefault); + } + + public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); + + public IPublishedContent GetSingleByXPath(string xpath, XPathVariable[] vars) + { + return GetSingleByXPath(PreviewDefault, xpath, vars); + } + + public abstract IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + + public IPublishedContent GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) + { + return GetSingleByXPath(PreviewDefault, xpath, vars); + } + + public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); + + public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) + { + return GetByXPath(PreviewDefault, xpath, vars); + } + + public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + + public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) + { + return GetByXPath(PreviewDefault, xpath, vars); + } + + public abstract XPathNavigator CreateNavigator(bool preview); + + public XPathNavigator CreateNavigator() + { + return CreateNavigator(PreviewDefault); + } + + public abstract XPathNavigator CreateNodeNavigator(int id, bool preview); + + public abstract bool HasContent(bool preview); + + public bool HasContent() + { + return HasContent(PreviewDefault); + } + + public abstract PublishedContentType GetContentType(int id); + + public abstract PublishedContentType GetContentType(string alias); + + public abstract IEnumerable GetByContentType(PublishedContentType contentType); + } +} diff --git a/src/Umbraco.Web/PublishedCache/PublishedCaches.cs b/src/Umbraco.Web/PublishedCache/PublishedCaches.cs deleted file mode 100644 index 377a891c4f..0000000000 --- a/src/Umbraco.Web/PublishedCache/PublishedCaches.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace Umbraco.Web.PublishedCache -{ - /// - /// Provides caches (content and media). - /// - /// Default implementation for unrelated caches. - internal class PublishedCaches : IPublishedCaches - { - private readonly IPublishedContentCache _contentCache; - private readonly IPublishedMediaCache _mediaCache; - - /// - /// Initializes a new instance of the class with a content cache - /// and a media cache. - /// - public PublishedCaches(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) - { - _contentCache = contentCache; - _mediaCache = mediaCache; - } - - /// - /// Creates a contextual content cache for a specified context. - /// - /// The context. - /// A new contextual content cache for the specified context. - public ContextualPublishedContentCache CreateContextualContentCache(UmbracoContext context) - { - return new ContextualPublishedContentCache(_contentCache, context); - } - - /// - /// Creates a contextual media cache for a specified context. - /// - /// The context. - /// A new contextual media cache for the specified context. - public ContextualPublishedMediaCache CreateContextualMediaCache(UmbracoContext context) - { - return new ContextualPublishedMediaCache(_mediaCache, context); - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs b/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs deleted file mode 100644 index f196e6a2d1..0000000000 --- a/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq.Expressions; -using LightInject; -using Umbraco.Core.ObjectResolution; - -namespace Umbraco.Web.PublishedCache -{ - //TODO: REmove this requirement, just use normal IoC and publicize IPublishedCaches - - /// - /// Resolves the IPublishedCaches object. - /// - internal sealed class PublishedCachesResolver : ContainerSingleObjectResolver - { - /// - /// Initializes the resolver to use IoC - /// - /// - /// - public PublishedCachesResolver(IServiceContainer container, Type implementationType) : base(container, implementationType) - { - } - - /// - /// Initializes a new instance of the class with caches. - /// - /// The caches. - /// The resolver is created by the WebBootManager and thus the constructor remains internal. - internal PublishedCachesResolver(IPublishedCaches caches) - : base(caches) - { } - - /// - /// Initializes the resolver to use IoC - /// - /// - /// - public PublishedCachesResolver(IServiceContainer container, Func implementationType) : base(container, implementationType) - { - } - - /// - /// Sets the caches. - /// - /// The caches. - /// For developers, at application startup. - public void SetCaches(IPublishedCaches caches) - { - Value = caches; - } - - /// - /// Gets the caches. - /// - public IPublishedCaches Caches - { - get { return Value; } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs new file mode 100644 index 0000000000..0f73ac95a7 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache +{ + // caches content, media and member types + + public class PublishedContentTypeCache + { + private readonly Dictionary _typesByAlias = new Dictionary(); + private readonly Dictionary _typesById = new Dictionary(); + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + internal PublishedContentTypeCache(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + } + + // for unit tests ONLY + internal PublishedContentTypeCache() + { } + + public void ClearAll() + { + Core.Logging.LogHelper.Debug("Clear all."); + + using (new WriteLock(_lock)) + { + _typesByAlias.Clear(); + _typesById.Clear(); + } + } + + public void ClearContentType(int id) + { + Core.Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); + + using (var l = new UpgradeableReadLock(_lock)) + { + PublishedContentType type; + if (_typesById.TryGetValue(id, out type) == false) + return; + + l.UpgradeToWriteLock(); + + _typesByAlias.Remove(GetAliasKey(type)); + _typesById.Remove(id); + } + } + + public void ClearDataType(int id) + { + Core.Logging.LogHelper.Debug("Clear data type w/id {0}.", () => id); + + // there is no recursion to handle here because a PublishedContentType contains *all* its + // properties ie both its own properties and those that were inherited (it's based upon an + // IContentTypeComposition) and so every PublishedContentType having a property based upon + // the cleared data type, be it local or inherited, will be cleared. + + using (new WriteLock(_lock)) + { + var toRemove = _typesById.Values.Where(x => x.PropertyTypes.Any(xx => xx.DataTypeId == id)).ToArray(); + foreach (var type in toRemove) + { + _typesByAlias.Remove(GetAliasKey(type)); + _typesById.Remove(type.Id); + } + } + } + + public PublishedContentType Get(PublishedItemType itemType, string alias) + { + var aliasKey = GetAliasKey(itemType, alias); + using (var l = new UpgradeableReadLock(_lock)) + { + PublishedContentType type; + if (_typesByAlias.TryGetValue(aliasKey, out type)) + return type; + type = CreatePublishedContentType(itemType, alias); + l.UpgradeToWriteLock(); + return _typesByAlias[aliasKey] = _typesById[type.Id] = type; + } + } + + public PublishedContentType Get(PublishedItemType itemType, int id) + { + using (var l = new UpgradeableReadLock(_lock)) + { + PublishedContentType type; + if (_typesById.TryGetValue(id, out type)) + return type; + type = CreatePublishedContentType(itemType, id); + l.UpgradeToWriteLock(); + return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; + } + } + + private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) + { + if (GetPublishedContentTypeByAlias != null) + return GetPublishedContentTypeByAlias(alias); + + IContentTypeComposition contentType; + switch (itemType) + { + case PublishedItemType.Content: + contentType = _contentTypeService.Get(alias); + break; + case PublishedItemType.Media: + contentType = _mediaTypeService.Get(alias); + break; + case PublishedItemType.Member: + contentType = _memberTypeService.Get(alias); + break; + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); + } + + if (contentType == null) + throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\"."); + + return new PublishedContentType(itemType, contentType); + } + + private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id) + { + if (GetPublishedContentTypeById != null) + return GetPublishedContentTypeById(id); + + IContentTypeComposition contentType; + switch (itemType) + { + case PublishedItemType.Content: + contentType = _contentTypeService.Get(id); + break; + case PublishedItemType.Media: + contentType = _mediaTypeService.Get(id); + break; + case PublishedItemType.Member: + contentType = _memberTypeService.Get(id); + break; + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); + } + + if (contentType == null) + throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}."); + + return new PublishedContentType(itemType, contentType); + } + + // for unit tests - changing the callback must reset the cache obviously + private Func _getPublishedContentTypeByAlias; + internal Func GetPublishedContentTypeByAlias + { + get { return _getPublishedContentTypeByAlias; } + set + { + using (new WriteLock(_lock)) + { + _typesByAlias.Clear(); + _typesById.Clear(); + _getPublishedContentTypeByAlias = value; + } + } + } + + // for unit tests - changing the callback must reset the cache obviously + private Func _getPublishedContentTypeById; + internal Func GetPublishedContentTypeById + { + get { return _getPublishedContentTypeById; } + set + { + using (new WriteLock(_lock)) + { + _typesByAlias.Clear(); + _typesById.Clear(); + _getPublishedContentTypeById = value; + } + } + } + + private static string GetAliasKey(PublishedItemType itemType, string alias) + { + string k; + + if (itemType == PublishedItemType.Content) + k = "c"; + else if (itemType == PublishedItemType.Media) + k = "m"; + else if (itemType == PublishedItemType.Member) + k = "m"; + else throw new ArgumentOutOfRangeException(nameof(itemType)); + + return k + ":" + alias; + } + + private static string GetAliasKey(PublishedContentType contentType) + { + return GetAliasKey(contentType.ItemType, contentType.Alias); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs similarity index 90% rename from src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs rename to src/Umbraco.Web/PublishedCache/PublishedMember.cs index 07fa13a6d8..cda7291769 100644 --- a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -1,17 +1,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; using System.Linq; -using System.Text; -using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; using Umbraco.Web.Models; namespace Umbraco.Web.PublishedCache @@ -19,25 +13,21 @@ namespace Umbraco.Web.PublishedCache /// /// Exposes a member object as IPublishedContent /// - public sealed class MemberPublishedContent : PublishedContentWithKeyBase + public sealed class PublishedMember : PublishedContentWithKeyBase { - private readonly IMember _member; private readonly IMembershipUser _membershipUser; private readonly IPublishedProperty[] _properties; private readonly PublishedContentType _publishedMemberType; - public MemberPublishedContent(IMember member) + public PublishedMember(IMember member, PublishedContentType publishedMemberType) { - if (member == null) throw new ArgumentNullException("member"); + if (member == null) throw new ArgumentNullException(nameof(member)); + if (publishedMemberType == null) throw new ArgumentNullException(nameof(publishedMemberType)); _member = member; _membershipUser = member; - _publishedMemberType = PublishedContentType.Get(PublishedItemType.Member, _member.ContentTypeAlias); - if (_publishedMemberType == null) - { - throw new InvalidOperationException("Could not get member type with alias " + _member.ContentTypeAlias); - } + _publishedMemberType = publishedMemberType; _properties = PublishedProperty.MapProperties(_publishedMemberType.PropertyTypes, _member.Properties, (t, v) => new RawValueProperty(t, v ?? string.Empty)) @@ -89,7 +79,7 @@ namespace Umbraco.Web.PublishedCache public DateTime LastPasswordChangedDate { get { return _membershipUser.LastPasswordChangeDate; } - } + } #endregion #region IPublishedContent @@ -236,7 +226,7 @@ namespace Umbraco.Web.PublishedCache public override int Level { get { return _member.Level; } - } + } #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/RawValueProperty.cs b/src/Umbraco.Web/PublishedCache/RawValueProperty.cs index 5fa95f127b..59260e7d04 100644 --- a/src/Umbraco.Web/PublishedCache/RawValueProperty.cs +++ b/src/Umbraco.Web/PublishedCache/RawValueProperty.cs @@ -4,43 +4,45 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.PublishedCache { /// - /// A published property base that uses a raw object value + /// A published property base that uses a raw object value. /// + /// Conversions results are stored within the property and will not + /// be refreshed, so this class is not suitable for cached properties. internal class RawValueProperty : PublishedPropertyBase { private readonly object _dbVal; //the value in the db private readonly Lazy _sourceValue; private readonly Lazy _objectValue; private readonly Lazy _xpathValue; + private readonly bool _isPreviewing; - /// - /// Gets the raw value of the property. - /// - public override object DataValue { get { return _dbVal; } } - - public override bool HasValue - { - get { return _dbVal != null && _dbVal.ToString().Trim().Length > 0; } - } + public override object DataValue => _dbVal; - public override object Value { get { return _objectValue.Value; } } - public override object XPathValue { get { return _xpathValue.Value; } } - - public RawValueProperty(PublishedPropertyType propertyType, object propertyData) - : this(propertyType) + public override bool HasValue => _dbVal != null && _dbVal.ToString().Trim().Length > 0; + + public override object Value => _objectValue.Value; + + public override object XPathValue => _xpathValue.Value; + + // note: propertyData cannot be null + public RawValueProperty(PublishedPropertyType propertyType, object propertyData, bool isPreviewing = false) + : this(propertyType, isPreviewing) { if (propertyData == null) - throw new ArgumentNullException("propertyData"); + throw new ArgumentNullException(nameof(propertyData)); _dbVal = propertyData; } - public RawValueProperty(PublishedPropertyType propertyType) + // note: maintaining two ctors to make sure we understand what we do when calling them + public RawValueProperty(PublishedPropertyType propertyType, bool isPreviewing = false) : base(propertyType) { _dbVal = null; - _sourceValue = new Lazy(() => PropertyType.ConvertDataToSource(_dbVal, false)); - _objectValue = new Lazy(() => PropertyType.ConvertSourceToObject(_sourceValue.Value, false)); - _xpathValue = new Lazy(() => PropertyType.ConvertSourceToXPath(_sourceValue.Value, false)); + _isPreviewing = isPreviewing; + + _sourceValue = new Lazy(() => PropertyType.ConvertDataToSource(_dbVal, _isPreviewing)); + _objectValue = new Lazy(() => PropertyType.ConvertSourceToObject(_sourceValue.Value, _isPreviewing)); + _xpathValue = new Lazy(() => PropertyType.ConvertSourceToXPath(_sourceValue.Value, _isPreviewing)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs new file mode 100644 index 0000000000..83651a9986 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class DomainCache : IDomainCache + { + private readonly IDomainService _domainService; + + public DomainCache(IDomainService domainService) + { + _domainService = domainService; + } + + public IEnumerable GetAll(bool includeWildcards) + { + return _domainService.GetAll(includeWildcards) + .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) + .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)); + } + + public IEnumerable GetAssigned(int contentId, bool includeWildcards) + { + return _domainService.GetAssignedDomains(contentId, includeWildcards) + .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) + .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs new file mode 100644 index 0000000000..0b2f331b14 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs @@ -0,0 +1,56 @@ +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// Implements a facade. + /// + class Facade : IFacade + { + /// + /// Initializes a new instance of the class with a content cache + /// and a media cache. + /// + public Facade( + PublishedContentCache contentCache, + PublishedMediaCache mediaCache, + PublishedMemberCache memberCache, + DomainCache domainCache) + { + ContentCache = contentCache; + MediaCache = mediaCache; + MemberCache = memberCache; + DomainCache = domainCache; + } + + /// + /// Gets the . + /// + public IPublishedContentCache ContentCache { get; } + + /// + /// Gets the . + /// + public IPublishedMediaCache MediaCache { get; } + + /// + /// Gets the . + /// + public IPublishedMemberCache MemberCache { get; } + + /// + /// Gets the . + /// + public IDomainCache DomainCache { get; } + + public static void ResyncCurrent() + { + if (FacadeServiceResolver.HasCurrent == false) return; + var service = FacadeServiceResolver.Current.Service as FacadeService; + var facade = service?.GetFacade() as Facade; + if (facade == null) return; + ((PublishedContentCache) facade.ContentCache).Resync(); + ((PublishedMediaCache)facade.MediaCache).Resync(); + + // not trying to resync members or domains, which are not cached really + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs new file mode 100644 index 0000000000..83d12759de --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs @@ -0,0 +1,187 @@ +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Web.Cache; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// Implements a facade service. + /// + class FacadeService : FacadeServiceBase + { + private readonly XmlStore _xmlStore; + private readonly RoutesCache _routesCache; + private readonly PublishedContentTypeCache _contentTypeCache; + private readonly IDomainService _domainService; + private readonly IMemberService _memberService; + private readonly IMediaService _mediaService; + private readonly ICacheProvider _requestCache; + + #region Constructors + + // used in StandaloneBootManager only, should get rid of that one eventually + internal FacadeService(ServiceContext serviceContext, IDatabaseUnitOfWorkProvider uowProvider, ICacheProvider requestCache) + : this(serviceContext, uowProvider, requestCache, null, false, true) + { } + + // used in some tests + in WebBootManager + internal FacadeService(ServiceContext serviceContext, IDatabaseUnitOfWorkProvider uowProvider, ICacheProvider requestCache, + bool testing, bool enableRepositoryEvents) + : this(serviceContext, uowProvider, requestCache, null, testing, enableRepositoryEvents) + { } + + // used in some tests + internal FacadeService(ServiceContext serviceContext, IDatabaseUnitOfWorkProvider uowProvider, ICacheProvider requestCache, + PublishedContentTypeCache contentTypeCache, bool testing, bool enableRepositoryEvents) + { + _routesCache = new RoutesCache(); + _contentTypeCache = contentTypeCache + ?? new PublishedContentTypeCache(serviceContext.ContentTypeService, serviceContext.MediaTypeService, serviceContext.MemberTypeService); + + var providers = UrlSegmentProviderResolver.Current.Providers; // fixme - inject! + _xmlStore = new XmlStore(serviceContext, uowProvider, _routesCache, _contentTypeCache, providers, testing, enableRepositoryEvents); + + _domainService = serviceContext.DomainService; + _memberService = serviceContext.MemberService; + _mediaService = serviceContext.MediaService; + _requestCache = requestCache; + } + + public override void Dispose() + { + _xmlStore.Dispose(); + } + + #endregion + + #region PublishedCachesService Caches + + public override IFacade CreateFacade(string previewToken) + { + // use _requestCache to store recursive properties lookup, etc. both in content + // and media cache. Life span should be the current request. Or, ideally + // the current caches, but that would mean creating an extra cache (StaticCache + // probably) so better use RequestCache. + + var domainCache = new DomainCache(_domainService); + + return new Facade( + new PublishedContentCache(_xmlStore, domainCache, _requestCache, _contentTypeCache, _routesCache, previewToken), + new PublishedMediaCache(_xmlStore, _mediaService, _requestCache, _contentTypeCache), + new PublishedMemberCache(_xmlStore, _requestCache, _memberService, _contentTypeCache), + domainCache); + } + + #endregion + + #region PublishedCachesService Preview + + public override string EnterPreview(IUser user, int contentId) + { + var previewContent = new PreviewContent(_xmlStore, user.Id); + previewContent.CreatePreviewSet(contentId, true); // preview branch below that content + return previewContent.Token; + //previewContent.ActivatePreviewCookie(); + } + + public override void RefreshPreview(string previewToken, int contentId) + { + if (previewToken.IsNullOrWhiteSpace()) return; + var previewContent = new PreviewContent(_xmlStore, previewToken); + previewContent.CreatePreviewSet(contentId, true); // preview branch below that content + } + + public override void ExitPreview(string previewToken) + { + if (previewToken.IsNullOrWhiteSpace()) return; + var previewContent = new PreviewContent(_xmlStore, previewToken); + previewContent.ClearPreviewSet(); + } + + #endregion + + #region Xml specific + + /// + /// Gets the underlying XML store. + /// + public XmlStore XmlStore => _xmlStore; + + /// + /// Gets the underlying RoutesCache. + /// + public RoutesCache RoutesCache => _routesCache; + + public bool VerifyContentAndPreviewXml() + { + return XmlStore.VerifyContentAndPreviewXml(); + } + + public void RebuildContentAndPreviewXml() + { + XmlStore.RebuildContentAndPreviewXml(); + } + + public bool VerifyMediaXml() + { + return XmlStore.VerifyMediaXml(); + } + + public void RebuildMediaXml() + { + XmlStore.RebuildMediaXml(); + } + + public bool VerifyMemberXml() + { + return XmlStore.VerifyMemberXml(); + } + + public void RebuildMemberXml() + { + XmlStore.RebuildMemberXml(); + } + + #endregion + + #region Change management + + public override void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) + { + _xmlStore.Notify(payloads, out draftChanged, out publishedChanged); + } + + public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) + { + foreach (var payload in payloads) + PublishedMediaCache.ClearCache(payload.Id); + + anythingChanged = true; + } + + public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) + { + _xmlStore.Notify(payloads); + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) + _routesCache.Clear(); + } + + public override void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) + { + _xmlStore.Notify(payloads); + } + + public override void Notify(DomainCacheRefresher.JsonPayload[] payloads) + { + _routesCache.Clear(); + } + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PreviewContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PreviewContent.cs new file mode 100644 index 0000000000..57dbbb4c36 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PreviewContent.cs @@ -0,0 +1,162 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PreviewContent + { + private readonly int _userId; + private readonly Guid _previewSet; + private string _previewSetPath; + private XmlDocument _previewXml; + private readonly XmlStore _xmlStore; + + /// + /// Gets the XML document. + /// + /// May return null if the preview content set is invalid. + public XmlDocument XmlContent + { + get + { + // null if invalid preview content + if (_previewSetPath == null) return null; + + // load if not loaded yet + if (_previewXml != null) + return _previewXml; + + _previewXml = new XmlDocument(); + + try + { + _previewXml.Load(_previewSetPath); + } + catch (Exception ex) + { + LogHelper.Error($"Could not load preview set {_previewSet} for user {_userId}.", ex); + + ClearPreviewSet(); + + _previewXml = null; + _previewSetPath = null; // do not try again + } + + return _previewXml; + } + } + + /// + /// Gets the preview token. + /// + /// To be stored in a cookie or wherever appropriate. + public string Token => _userId + ":" + _previewSet; + + /// + /// Initializes a new instance of the class for a user. + /// + /// The underlying Xml store. + /// The user identifier. + public PreviewContent(XmlStore xmlStore, int userId) + { + if (xmlStore == null) + throw new ArgumentNullException(nameof(xmlStore)); + _xmlStore = xmlStore; + + _userId = userId; + _previewSet = Guid.NewGuid(); + _previewSetPath = GetPreviewSetPath(_userId, _previewSet); + } + + /// + /// Initializes a new instance of the with a preview token. + /// + /// The underlying Xml store. + /// The preview token. + public PreviewContent(XmlStore xmlStore, string token) + { + if (xmlStore == null) + throw new ArgumentNullException(nameof(xmlStore)); + _xmlStore = xmlStore; + + if (token.IsNullOrWhiteSpace()) + throw new ArgumentException("Null or empty token.", nameof(token)); + var parts = token.Split(':'); + if (parts.Length != 2) + throw new ArgumentException("Invalid token.", nameof(token)); + + if (int.TryParse(parts[0], out _userId) == false) + throw new ArgumentException("Invalid token.", nameof(token)); + if (Guid.TryParse(parts[1], out _previewSet) == false) + throw new ArgumentException("Invalid token.", nameof(token)); + + _previewSetPath = GetPreviewSetPath(_userId, _previewSet); + } + + // creates and saves a new preview set + // used in 2 places and each time includeSubs is true + // have to use the Document class at the moment because IContent does not do ToXml... + public void CreatePreviewSet(int contentId, bool includeSubs) + { + // note: always include subs + _previewXml = _xmlStore.GetPreviewXml(contentId, includeSubs); + + // make sure the preview folder exists + var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview)); + if (dir.Exists == false) + dir.Create(); + + // clean old preview sets + ClearPreviewDirectory(_userId, dir); + + // save + _previewXml.Save(_previewSetPath); + } + + // get the full path to the preview set + private static string GetPreviewSetPath(int userId, Guid previewSet) + { + return IOHelper.MapPath(Path.Combine(SystemDirectories.Preview, userId + "_" + previewSet + ".config")); + } + + // deletes files for the user, and files accessed more than one hour ago + private static void ClearPreviewDirectory(int userId, DirectoryInfo dir) + { + var now = DateTime.Now; + var prefix = userId + "_"; + foreach (var file in dir.GetFiles("*.config") + .Where(x => x.Name.StartsWith(prefix) || (now - x.LastAccessTime).TotalMinutes > 1)) + { + DeletePreviewSetFile(userId, file); + } + } + + // delete one preview set file in a safe way + private static void DeletePreviewSetFile(int userId, FileSystemInfo file) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + LogHelper.Error($"Couldn't delete preview set {file.Name} for user {userId}", ex); + } + } + + /// + /// Deletes the preview set in a safe way. + /// + public void ClearPreviewSet() + { + if (_previewSetPath == null) return; + var previewSetFile = new FileInfo(_previewSetPath); + DeletePreviewSetFile(_userId, previewSetFile); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index a43c734949..8b3fd79252 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -17,89 +17,84 @@ using umbraco; using System.Linq; using System.Web; using umbraco.BusinessLogic; -using umbraco.presentation.preview; +using Umbraco.Core.Cache; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { - internal class PublishedContentCache : IPublishedContentCache + internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache { - /// - /// Constructor - /// - /// - /// - /// Use this ctor for unit tests in order to supply a custom xml result - /// - public PublishedContentCache(Func getXmlDelegate) - { - if (getXmlDelegate == null) throw new ArgumentNullException("getXmlDelegate"); - _xmlDelegate = getXmlDelegate; - } + private readonly ICacheProvider _cacheProvider; + private readonly RoutesCache _routesCache; + private readonly IDomainCache _domainCache; + private readonly DomainHelper _domainHelper; + private readonly PublishedContentTypeCache _contentTypeCache; - /// - /// Constructor - /// - /// - /// Using this ctor will ONLY work in a web/http context - /// - public PublishedContentCache() + // initialize a PublishedContentCache instance with + // an XmlStore containing the master xml + // an ICacheProvider that should be at request-level + // a RoutesCache - need to cleanup that one + // a preview token string (or null if not previewing) + public PublishedContentCache( + XmlStore xmlStore, // an XmlStore containing the master xml + IDomainCache domainCache, // an IDomainCache implementation + ICacheProvider cacheProvider, // an ICacheProvider that should be at request-level + PublishedContentTypeCache contentTypeCache, // a PublishedContentType cache + RoutesCache routesCache, // a RoutesCache + string previewToken) // a preview token string (or null if not previewing) + : base(previewToken.IsNullOrWhiteSpace() == false) { - _xmlDelegate = ((context, preview) => - { - if (preview) - { - var previewContent = PreviewContentCache.GetOrCreateValue(context); // will use the ctor with no parameters - var previewVal = HttpContext.Current.Request.GetPreviewCookieValue(); - previewContent.EnsureInitialized(context.Security.CurrentUser, previewVal, true, () => - { - if (previewContent.ValidPreviewSet) - previewContent.LoadPreviewset(); - }); - if (previewContent.ValidPreviewSet) - return previewContent.XmlContent; - } - return content.Instance.XmlContent; - }); + _cacheProvider = cacheProvider; + _routesCache = routesCache; // may be null for unit-testing + _contentTypeCache = contentTypeCache; + _domainCache = domainCache; + _domainHelper = new DomainHelper(_domainCache); + + _xmlStore = xmlStore; + _xml = _xmlStore.Xml; // capture - because the cache has to remain consistent + + if (previewToken.IsNullOrWhiteSpace() == false) + _previewContent = new PreviewContent(_xmlStore, previewToken); } - #region Routes cache - private readonly RoutesCache _routesCache = new RoutesCache(!UnitTesting); + #region Unit Tests // for INTERNAL, UNIT TESTS use ONLY - internal RoutesCache RoutesCache { get { return _routesCache; } } + internal RoutesCache RoutesCache => _routesCache; // for INTERNAL, UNIT TESTS use ONLY - internal static bool UnitTesting = false; + internal XmlStore XmlStore => _xmlStore; - public virtual IPublishedContent GetByRoute(UmbracoContext umbracoContext, bool preview, string route, bool? hideTopLevelNode = null) + #endregion + + #region Routes + + public virtual IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null) { - if (route == null) throw new ArgumentNullException("route"); + if (route == null) throw new ArgumentNullException(nameof(route)); // try to get from cache if not previewing - var contentId = preview ? 0 : _routesCache.GetNodeId(route); + var contentId = (preview || _routesCache == null) ? 0 : _routesCache.GetNodeId(route); // if found id in cache then get corresponding content // and clear cache if not found - for whatever reason IPublishedContent content = null; if (contentId > 0) { - content = GetById(umbracoContext, preview, contentId); + content = GetById(preview, contentId); if (content == null) - _routesCache.ClearNode(contentId); + _routesCache?.ClearNode(contentId); } // still have nothing? actually determine the id hideTopLevelNode = hideTopLevelNode ?? GlobalSettings.HideTopLevelNodeFromPath; // default = settings - content = content ?? DetermineIdByRoute(umbracoContext, preview, route, hideTopLevelNode.Value); + content = content ?? DetermineIdByRoute(preview, route, hideTopLevelNode.Value); // cache if we have a content and not previewing - if (content != null && preview == false) + if (content != null && preview == false && _routesCache != null) { var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/'))); - var iscanon = - UnitTesting == false - && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false; + var iscanon = DomainHelper.ExistsDomainInPath(_domainCache.GetAll(false), content.Path, domainRootNodeId) == false; // and only if this is the canonical url (the one GetUrl would return) if (iscanon) _routesCache.Store(content.Id, route); @@ -108,28 +103,38 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return content; } - public virtual string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId) + public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null) + { + return GetByRoute(PreviewDefault, route, hideTopLevelNode); + } + + public virtual string GetRouteById(bool preview, int contentId) { // try to get from cache if not previewing - var route = preview ? null : _routesCache.GetRoute(contentId); + var route = (preview || _routesCache == null) ? null : _routesCache.GetRoute(contentId); // if found in cache then return if (route != null) return route; // else actually determine the route - route = DetermineRouteById(umbracoContext, preview, contentId); + route = DetermineRouteById(preview, contentId); // cache if we have a route and not previewing if (route != null && preview == false) - _routesCache.Store(contentId, route); + _routesCache?.Store(contentId, route); return route; } - IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, bool preview, string route, bool hideTopLevelNode) + public string GetRouteById(int contentId) { - if (route == null) throw new ArgumentNullException("route"); + return GetRouteById(PreviewDefault, contentId); + } + + IPublishedContent DetermineIdByRoute(bool preview, string route, bool hideTopLevelNode) + { + if (route == null) throw new ArgumentNullException(nameof(route)); //the route always needs to be lower case because we only store the urlName attribute in lower case route = route.ToLowerInvariant(); @@ -142,7 +147,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode, out vars); //check if we can find the node in our xml cache - var content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + var content = GetSingleByXPath(preview, xpath, vars?.ToArray()); // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo // but maybe that was the url of a non-default top-level node, so we also @@ -150,25 +155,23 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (content == null && hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) { xpath = CreateXpathQuery(startNodeId, path, false, out vars); - content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + content = GetSingleByXPath(preview, xpath, vars?.ToArray()); } return content; } - string DetermineRouteById(UmbracoContext umbracoContext, bool preview, int contentId) + string DetermineRouteById(bool preview, int contentId) { - var node = GetById(umbracoContext, preview, contentId); + var node = GetById(preview, contentId); if (node == null) return null; - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); - // walk up from that node until we hit a node with a domain, // or we reach the content root, collecting urls in the way var pathParts = new List(); var n = node; - var hasDomains = domainHelper.NodeHasDomains(n.Id); + var hasDomains = _domainHelper.NodeHasDomains(n.Id); while (hasDomains == false && n != null) // n is null at root { // get the url @@ -177,22 +180,22 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // move to parent node n = n.Parent; - hasDomains = n != null && domainHelper.NodeHasDomains(n.Id); + hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id); } // no domain, respect HideTopLevelNodeFromPath for legacy purposes if (hasDomains == false && GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(umbracoContext, node, pathParts); + ApplyHideTopLevelNodeFromPath(node, pathParts, preview); // assemble the route pathParts.Reverse(); var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path; + var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path; return route; } - static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContent node, IList pathParts) + void ApplyHideTopLevelNodeFromPath(IPublishedContent content, IList segments, bool preview) { // in theory if hideTopLevelNodeFromPath is true, then there should be only once // top-level node, or else domains should be assigned. but for backward compatibility @@ -202,17 +205,17 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // "/foo" fails (looking for "/*/foo") we try also "/foo". // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (node.Parent == null) + if (content.Parent == null) { - var rootNode = umbracoContext.ContentCache.GetByRoute("/", true); + var rootNode = GetByRoute(preview, "/", true); if (rootNode == null) throw new Exception("Failed to get node at /."); - if (rootNode.Id == node.Id) // remove only if we're the default node - pathParts.RemoveAt(pathParts.Count - 1); + if (rootNode.Id == content.Id) // remove only if we're the default node + segments.RemoveAt(segments.Count - 1); } else { - pathParts.RemoveAt(pathParts.Count - 1); + segments.RemoveAt(segments.Count - 1); } } @@ -220,177 +223,187 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache #region XPath Strings - class XPathStringsDefinition - { - public int Version { get; private set; } + static class XPathStrings + { + public const string Root = "/root"; + public const string RootDocuments = "/root/* [@isDoc]"; + public const string DescendantDocumentById = "//* [@isDoc and @id={0}]"; + public const string ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']"; + public const string ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]"; + public const string RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]"; + } - public static string Root { get { return "/root"; } } - public string RootDocuments { get; private set; } - public string DescendantDocumentById { get; private set; } - public string ChildDocumentByUrlName { get; private set; } - public string ChildDocumentByUrlNameVar { get; private set; } - public string RootDocumentWithLowestSortOrder { get; private set; } - - public XPathStringsDefinition(int version) - { - Version = version; - - switch (version) - { - // legacy XML schema - case 0: - RootDocuments = "/root/node"; - DescendantDocumentById = "//node [@id={0}]"; - ChildDocumentByUrlName = "/node [@urlName='{0}']"; - ChildDocumentByUrlNameVar = "/node [@urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]"; - break; - - // default XML schema as of 4.10 - case 1: - RootDocuments = "/root/* [@isDoc]"; - DescendantDocumentById = "//* [@isDoc and @id={0}]"; - ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']"; - ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]"; - break; - - default: - throw new Exception(string.Format("Unsupported Xml schema version '{0}').", version)); - } - } - } - - static XPathStringsDefinition _xPathStringsValue; - static XPathStringsDefinition XPathStrings - { - get - { - // in theory XPathStrings should be a static variable that - // we should initialize in a static ctor - but then test cases - // that switch schemas fail - so cache and refresh when needed, - // ie never when running the actual site - - var version = 1; - if (_xPathStringsValue == null || _xPathStringsValue.Version != version) - _xPathStringsValue = new XPathStringsDefinition(version); - return _xPathStringsValue; - } - } - - #endregion + #endregion #region Converters - private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing) + private IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing, ICacheProvider cacheProvider) { return xmlNode == null ? null - : (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel(); + : (new XmlPublishedContent(xmlNode, isPreviewing, cacheProvider, _contentTypeCache)).CreateModel(); } - private static IEnumerable ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing) + private IEnumerable ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing, ICacheProvider cacheProvider) { return xmlNodes.Cast() - .Select(xmlNode => (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel()); + .Select(xmlNode => (new XmlPublishedContent(xmlNode, isPreviewing, cacheProvider, _contentTypeCache)).CreateModel()); } #endregion #region Getters - public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId) + public override IPublishedContent GetById(bool preview, int nodeId) { - return ConvertToDocument(GetXml(umbracoContext, preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview); + return ConvertToDocument(GetXml(preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview, _cacheProvider); } - public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) + public override bool HasById(bool preview, int contentId) { - return ConvertToDocuments(GetXml(umbracoContext, preview).SelectNodes(XPathStrings.RootDocuments), preview); + return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture)); + } + + public override IEnumerable GetAtRoot(bool preview) + { + return ConvertToDocuments(GetXml(preview).SelectNodes(XPathStrings.RootDocuments), preview, _cacheProvider); } - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars) + public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) { - if (xpath == null) throw new ArgumentNullException("xpath"); + if (xpath == null) throw new ArgumentNullException(nameof(xpath)); if (string.IsNullOrWhiteSpace(xpath)) return null; - var xml = GetXml(umbracoContext, preview); + var xml = GetXml(preview); var node = vars == null ? xml.SelectSingleNode(xpath) : xml.SelectSingleNode(xpath, vars); - return ConvertToDocument(node, preview); + return ConvertToDocument(node, preview, _cacheProvider); } - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars) + public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) { - if (xpath == null) throw new ArgumentNullException("xpath"); + if (xpath == null) throw new ArgumentNullException(nameof(xpath)); - var xml = GetXml(umbracoContext, preview); + var xml = GetXml(preview); var node = vars == null ? xml.SelectSingleNode(xpath) : xml.SelectSingleNode(xpath, vars); - return ConvertToDocument(node, preview); + return ConvertToDocument(node, preview, _cacheProvider); } - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars) + public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) { - if (xpath == null) throw new ArgumentNullException("xpath"); + if (xpath == null) throw new ArgumentNullException(nameof(xpath)); if (string.IsNullOrWhiteSpace(xpath)) return Enumerable.Empty(); - var xml = GetXml(umbracoContext, preview); + var xml = GetXml(preview); var nodes = vars == null ? xml.SelectNodes(xpath) : xml.SelectNodes(xpath, vars); - return ConvertToDocuments(nodes, preview); + return ConvertToDocuments(nodes, preview, _cacheProvider); } - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars) + public override IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) { - if (xpath == null) throw new ArgumentNullException("xpath"); + if (xpath == null) throw new ArgumentNullException(nameof(xpath)); - var xml = GetXml(umbracoContext, preview); + var xml = GetXml(preview); var nodes = vars == null ? xml.SelectNodes(xpath) : xml.SelectNodes(xpath, vars); - return ConvertToDocuments(nodes, preview); + return ConvertToDocuments(nodes, preview, _cacheProvider); } - public virtual bool HasContent(UmbracoContext umbracoContext, bool preview) + public override bool HasContent(bool preview) { - var xml = GetXml(umbracoContext, preview); - if (xml == null) - return false; - var node = xml.SelectSingleNode(XPathStrings.RootDocuments); + var xml = GetXml(preview); + var node = xml?.SelectSingleNode(XPathStrings.RootDocuments); return node != null; } - public virtual XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview) + public override XPathNavigator CreateNavigator(bool preview) { - var xml = GetXml(umbracoContext, preview); + var xml = GetXml(preview); return xml.CreateNavigator(); } - public virtual bool XPathNavigatorIsNavigable { get { return false; } } + public override XPathNavigator CreateNodeNavigator(int id, bool preview) + { + // hackish - backward compatibility ;-( + + XPathNavigator navigator = null; + + if (preview) + { + var node = _xmlStore.GetPreviewXmlNode(id); + if (node != null) + { + navigator = node.CreateNavigator(); + } + } + else + { + var node = GetXml(false).GetElementById(id.ToInvariantString()); + if (node != null) + { + var doc = new XmlDocument(); + var clone = doc.ImportNode(node, false); + var props = node.SelectNodes("./* [not(@id)]"); + if (props == null) throw new Exception("oops"); + foreach (var n in props.Cast()) + clone.AppendChild(doc.ImportNode(n, true)); + navigator = node.CreateNavigator(); + } + } + + return navigator; + } #endregion #region Legacy Xml - static readonly ConditionalWeakTable PreviewContentCache - = new ConditionalWeakTable(); + private readonly XmlStore _xmlStore; + private XmlDocument _xml; + private readonly PreviewContent _previewContent; - private readonly Func _xmlDelegate; - - internal XmlDocument GetXml(UmbracoContext umbracoContext, bool preview) + internal XmlDocument GetXml(bool preview) { - return _xmlDelegate(umbracoContext, preview); + // not trying to be thread-safe here, that's not the point + + if (preview == false) + return _xml; + + // Xml cache does not support retrieving preview content when not previewing + if (_previewContent == null) + throw new InvalidOperationException("Cannot retrieve preview content when not previewing."); + + // PreviewContent tries to load the Xml once and if it fails, + // it invalidates itself and always return null for XmlContent. + var previewXml = _previewContent.XmlContent; + return previewXml ?? _xml; + } + + internal void Resync() + { + _xml = _xmlStore.Xml; // re-capture + + // note: we're not resyncing "preview" because that would mean re-building the whole + // preview set which is costly, so basically when previewing, there will be no resync. + + // clear recursive properties cached by XmlPublishedContent.GetProperty + // assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch) + // NOTE also clears all the media cache properties, which is OK (see media cache) + _cacheProvider.ClearCacheObjectTypes(); + //_cacheProvider.ClearCacheByKeySearch("XmlPublishedCache.PublishedContentCache:RecursiveProperty-"); } #endregion #region XPathQuery - static readonly char[] SlashChar = new[] { '/' }; + static readonly char[] SlashChar = { '/' }; protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable vars) { @@ -403,7 +416,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (startNodeId > 0) { // if in a domain then use the root node of the domain - xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); + xpath = string.Format(XPathStrings.Root + XPathStrings.DescendantDocumentById, startNodeId); } else { @@ -428,19 +441,17 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // if url is not empty, then use it to try lookup a matching page var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); var xpathBuilder = new StringBuilder(); - int partsIndex = 0; + var partsIndex = 0; List varsList = null; if (startNodeId == 0) { - if (hideTopLevelNodeFromPath) - xpathBuilder.Append(XPathStrings.RootDocuments); // first node is not in the url - else - xpathBuilder.Append(XPathStringsDefinition.Root); + // if hiding, first node is not in the url + xpathBuilder.Append(hideTopLevelNodeFromPath ? XPathStrings.RootDocuments : XPathStrings.Root); } else { - xpathBuilder.AppendFormat(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); + xpathBuilder.AppendFormat(XPathStrings.Root + XPathStrings.DescendantDocumentById, startNodeId); // always "hide top level" when there's a domain } @@ -451,7 +462,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { // use vars, escaping gets ugly pretty quickly varsList = varsList ?? new List(); - var varName = string.Format("var{0}", partsIndex); + var varName = $"var{partsIndex}"; varsList.Add(new XPathVariable(varName, part)); xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlNameVar, varName); } @@ -477,10 +488,29 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public IPublishedProperty CreateDetachedProperty(PublishedPropertyType propertyType, object value, bool isPreviewing) { if (propertyType.IsDetachedOrNested == false) - throw new ArgumentException("Property type is neither detached nor nested.", "propertyType"); + throw new ArgumentException("Property type is neither detached nor nested.", nameof(propertyType)); return new XmlPublishedProperty(propertyType, isPreviewing, value.ToString()); } #endregion + + #region Content types + + public override PublishedContentType GetContentType(int id) + { + return _contentTypeCache.Get(PublishedItemType.Content, id); + } + + public override PublishedContentType GetContentType(string alias) + { + return _contentTypeCache.Get(PublishedItemType.Content, alias); + } + + public override IEnumerable GetByContentType(PublishedContentType contentType) + { + throw new NotImplementedException(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index 3a61818ac9..5d2416c890 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -6,13 +6,10 @@ using System.IO; using System.Linq; using System.Xml.XPath; using Examine; -using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; using Examine.LuceneEngine.SearchCriteria; using Examine.Providers; -using Lucene.Net.Documents; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Dynamics; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -22,8 +19,7 @@ using Umbraco.Web.Models; using UmbracoExamine; using umbraco; using Umbraco.Core.Cache; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; +using Umbraco.Core.Services; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { @@ -33,83 +29,156 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly. /// - internal class PublishedMediaCache : IPublishedMediaCache + internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache { - public PublishedMediaCache(ApplicationContext applicationContext) - { - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - _applicationContext = applicationContext; - } + private readonly IMediaService _mediaService; + + // by default these are null unless specified by the ctor dedicated to tests + // when they are null the cache derives them from the ExamineManager, see + // method GetExamineManagerSafe(). + // + private readonly ILuceneSearcher _searchProvider; + private readonly BaseIndexProvider _indexProvider; + private readonly XmlStore _xmlStore; + private readonly PublishedContentTypeCache _contentTypeCache; + + // must be specified by the ctor + private readonly ICacheProvider _cacheProvider; + + public PublishedMediaCache(XmlStore xmlStore, IMediaService mediaService, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache) + : base(false) + { + if (mediaService == null) throw new ArgumentNullException(nameof(mediaService)); + _mediaService = mediaService; + _cacheProvider = cacheProvider; + _xmlStore = xmlStore; + _contentTypeCache = contentTypeCache; + } /// /// Generally used for unit testing to use an explicit examine searcher /// - /// + /// /// /// - internal PublishedMediaCache(ApplicationContext applicationContext, ILuceneSearcher searchProvider, BaseIndexProvider indexProvider) + /// + /// + internal PublishedMediaCache(IMediaService mediaService, ILuceneSearcher searchProvider, BaseIndexProvider indexProvider, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache) + : base(false) { - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - if (searchProvider == null) throw new ArgumentNullException("searchProvider"); - if (indexProvider == null) throw new ArgumentNullException("indexProvider"); + if (mediaService == null) throw new ArgumentNullException(nameof(mediaService)); + if (searchProvider == null) throw new ArgumentNullException(nameof(searchProvider)); + if (indexProvider == null) throw new ArgumentNullException(nameof(indexProvider)); - _applicationContext = applicationContext; + _mediaService = mediaService; _searchProvider = searchProvider; _indexProvider = indexProvider; - } + _cacheProvider = cacheProvider; + _contentTypeCache = contentTypeCache; + } static PublishedMediaCache() { InitializeCacheConfig(); } - private readonly ApplicationContext _applicationContext; - private readonly ILuceneSearcher _searchProvider; - private readonly BaseIndexProvider _indexProvider; - - public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId) + public override IPublishedContent GetById(bool preview, int nodeId) { return GetUmbracoMedia(nodeId); } - public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) + public override bool HasById(bool preview, int contentId) + { + return GetUmbracoMedia(contentId) != null; + } + + public override IEnumerable GetAtRoot(bool preview) { //TODO: We should be able to look these ids first in Examine! - var rootMedia = _applicationContext.Services.MediaService.GetRootMedia(); + var rootMedia = _mediaService.GetRootMedia(); return rootMedia.Select(m => GetUmbracoMedia(m.Id)); } - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) + public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //var navigator = CreateNavigator(preview); + //var iterator = navigator.Select(xpath, vars); + //return GetSingleByXPath(iterator); } - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) + public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //var navigator = CreateNavigator(preview); + //var iterator = navigator.Select(xpath, vars); + //return GetSingleByXPath(iterator); } - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) + private IPublishedContent GetSingleByXPath(XPathNodeIterator iterator) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //if (iterator.MoveNext() == false) return null; + //var idAttr = iterator.Current.GetAttribute("id", ""); + //int id; + //return int.TryParse(idAttr, out id) ? GetUmbracoMedia(id) : null; } - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) + public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //var navigator = CreateNavigator(preview); + //var iterator = navigator.Select(xpath, vars); + //return GetByXPath(iterator); } - public virtual XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview) + public override IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //var navigator = CreateNavigator(preview); + //var iterator = navigator.Select(xpath, vars); + //return GetByXPath(iterator); } - public bool XPathNavigatorIsNavigable { get { return false; } } + private IEnumerable GetByXPath(XPathNodeIterator iterator) + { + while (iterator.MoveNext()) + { + var idAttr = iterator.Current.GetAttribute("id", ""); + int id; + if (int.TryParse(idAttr, out id)) + yield return GetUmbracoMedia(id); + } + } - public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } + public override XPathNavigator CreateNavigator(bool preview) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + //var doc = _xmlStore.GetMediaXml(); + //return doc.CreateNavigator(); + } - private ExamineManager GetExamineManagerSafe() + public override XPathNavigator CreateNodeNavigator(int id, bool preview) + { + // preview is ignored for media cache + + // this code is mostly used when replacing old media.ToXml() code, and that code + // stored the XML attached to the media itself - so for some time in memory - so + // unless we implement some sort of cache here, we're probably degrading perfs. + + XPathNavigator navigator = null; + var node = _xmlStore.GetMediaXmlNode(id); + if (node != null) + { + navigator = node.CreateNavigator(); + } + return navigator; + } + + public override bool HasContent(bool preview) { throw new NotImplementedException(); } + + private static ExamineManager GetExamineManagerSafe() { try { @@ -127,25 +196,24 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return _indexProvider; var eMgr = GetExamineManagerSafe(); - if (eMgr != null) - { - try - { - //by default use the InternalSearcher - var indexer = eMgr.IndexProviderCollection["InternalIndexer"]; - if (indexer.IndexerData.IncludeNodeTypes.Any() || indexer.IndexerData.ExcludeNodeTypes.Any()) - { - LogHelper.Warn("The InternalIndexer for examine is configured incorrectly, it should not list any include/exclude node types or field names, it should simply be configured as: " + ""); - } - return indexer; - } - catch (Exception ex) - { - LogHelper.Error("Could not retrieve the InternalIndexer", ex); - //something didn't work, continue returning null. - } - } - return null; + if (eMgr == null) return null; + + try + { + //by default use the InternalSearcher + var indexer = eMgr.IndexProviderCollection["InternalIndexer"]; + if (indexer.IndexerData.IncludeNodeTypes.Any() || indexer.IndexerData.ExcludeNodeTypes.Any()) + { + LogHelper.Warn("The InternalIndexer for examine is configured incorrectly, it should not list any include/exclude node types or field names, it should simply be configured as: " + ""); + } + return indexer; + } + catch (Exception ex) + { + LogHelper.Error("Could not retrieve the InternalIndexer", ex); + //something didn't work, continue returning null. + } + return null; } private ILuceneSearcher GetSearchProviderSafe() @@ -154,28 +222,27 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return _searchProvider; var eMgr = GetExamineManagerSafe(); - if (eMgr != null) - { - try - { - //by default use the InternalSearcher - return eMgr.GetSearcher(Constants.Examine.InternalIndexer); - } - catch (FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - //TODO: Need to fix examine in LB scenarios! - } - catch (NullReferenceException) - { - //This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore - // the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null - // reference error will occur because the examine settings are null. - } - } - return null; + if (eMgr == null) return null; + + try + { + //by default use the InternalSearcher + return eMgr.GetSearcher(Constants.Examine.InternalIndexer); + } + catch (FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + //TODO: Need to fix examine in LB scenarios! + } + catch (NullReferenceException) + { + //This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore + // the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null + // reference error will occur because the examine settings are null. + } + return null; } private IPublishedContent GetUmbracoMedia(int id) @@ -202,7 +269,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //first check in Examine as this is WAY faster var criteria = searchProvider.CreateSearchCriteria("media"); - var filter = criteria.Id(id).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + var filter = criteria.Id(id).Not().Field(BaseUmbracoIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. //+(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media @@ -226,14 +293,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache "Could not retrieve media {0} from Examine index, reverting to looking up media via legacy library.GetMedia method", () => id); - var media = global::umbraco.library.GetMedia(id, false); + var media = library.GetMedia(id, false); return ConvertFromXPathNodeIterator(media, id); } internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id) { - if (media != null && media.Current != null) + if (media?.Current != null) { return media.Current.Name.InvariantEquals("error") ? null @@ -249,8 +316,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache internal CacheValues ConvertFromSearchResult(SearchResult searchResult) { + // note: fixing fields in 7.x, removed by Shan for 8.0 var values = new Dictionary(searchResult.Fields); - + return new CacheValues { Values = values, @@ -260,7 +328,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false) { - if (xpath == null) throw new ArgumentNullException("xpath"); + if (xpath == null) throw new ArgumentNullException(nameof(xpath)); var values = new Dictionary {{"nodeName", xpath.GetAttribute("nodeName", "")}}; values["nodeTypeAlias"] = xpath.Name; @@ -272,13 +340,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (result.Current.MoveToFirstAttribute()) { //checking for duplicate keys because of the 'nodeTypeAlias' might already be added above. - if (!values.ContainsKey(result.Current.Name)) + if (values.ContainsKey(result.Current.Name) == false) { values[result.Current.Name] = result.Current.Value; } while (result.Current.MoveToNextAttribute()) { - if (!values.ContainsKey(result.Current.Name)) + if (values.ContainsKey(result.Current.Name) == false) { values[result.Current.Name] = result.Current.Value; } @@ -292,9 +360,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //add the user props while (result.MoveNext()) { - if (result.Current != null && !result.Current.HasAttributes) + if (result.Current != null && result.Current.HasAttributes == false) { - string value = result.Current.Value; + var value = result.Current.Value; if (string.IsNullOrEmpty(value)) { if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0) @@ -311,16 +379,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache Values = values, XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator! }; - - //var content = new DictionaryPublishedContent(values, - // d => d.ParentId != -1 //parent should be null if -1 - // ? GetUmbracoMedia(d.ParentId) - // : null, - // //callback to return the children of the current node based on the xml structure already found - // d => GetChildrenMedia(d.Id, xpath), - // GetProperty, - // false); - //return content.CreateModel(); } /// @@ -344,7 +402,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { //We are going to check for a special field however, that is because in some cases we store a 'Raw' //value in the index such as for xml/html. - var rawValue = dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias)); + var rawValue = dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(BaseUmbracoIndexer.RawFieldPrefix + alias)); return rawValue ?? dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); } @@ -378,7 +436,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media - //sort with the Sort field + // sort with the Sort field (updated for 8.0) var results = searchProvider.Find( filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile()); @@ -396,14 +454,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return medias; } - else - { - //if there's no result then return null. Previously we defaulted back to library.GetMedia below - //but this will always get called for when we are getting descendents since many items won't have - //children and then we are hitting the database again! - //So instead we're going to rely on Examine to have the correct results like it should. - return Enumerable.Empty(); - } + + //if there's no result then return null. Previously we defaulted back to library.GetMedia below + //but this will always get called for when we are getting descendents since many items won't have + //children and then we are hitting the database again! + //So instead we're going to rely on Examine to have the correct results like it should. + return Enumerable.Empty(); } catch (FileNotFoundException) { @@ -416,7 +472,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //falling back to get media var media = library.GetMedia(parentId, true); - if (media != null && media.Current != null) + if (media?.Current != null) { xpath = media.Current; } @@ -496,27 +552,33 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache Func getParent, Func> getChildren, Func getProperty, + ICacheProvider cacheProvider, + PublishedContentTypeCache contentTypeCache, XPathNavigator nav, bool fromExamine) { - if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); - if (getParent == null) throw new ArgumentNullException("getParent"); - if (getProperty == null) throw new ArgumentNullException("getProperty"); + 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 => _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.NodeTypeAliasFieldName); 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"); @@ -532,7 +594,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } }, "parentID"); - _contentType = PublishedContentType.Get(PublishedItemType.Media, _documentTypeAlias); + _contentType = contentTypeCache.Get(PublishedItemType.Media, _documentTypeAlias); _properties = new Collection(); //handle content type properties @@ -570,194 +632,111 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private DateTime ParseDateTimeValue(string val) { - if (LoadedFromExamine) - { - //we need to parse the date time using Lucene converters - var ticks = long.Parse(val); - return new DateTime(ticks); - } + if (LoadedFromExamine == false) + return DateTime.Parse(val); - 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 set; } + 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 - { - get { return PublishedItemType.Media; } - } + public override PublishedItemType ItemType => PublishedItemType.Media; - public override IPublishedContent Parent - { - get { return _getParent.Value; } - } + public override IPublishedContent Parent => _getParent.Value; - public int ParentId { get; private set; } - public override int Id - { - get { return _id; } - } + public int ParentId { get; private set; } - public override Guid Key { get { return _key; } } + public override int Id => _id; - public override int TemplateId - { - get { return 0; } - } + public override Guid Key => _key; - public override int SortOrder - { - get { return _sortOrder; } - } + public override int TemplateId => 0; - public override string Name - { - get { return _name; } - } + public override int SortOrder => _sortOrder; - public override string UrlName - { - get { return _urlName; } - } + public override string Name => _name; - public override string DocumentTypeAlias - { - get { return _documentTypeAlias; } - } + public override string UrlName => _urlName; - public override int DocumentTypeId - { - get { return _documentTypeId; } - } + public override string DocumentTypeAlias => _documentTypeAlias; - public override string WriterName - { - get { return _creatorName; } - } + public override int DocumentTypeId => _documentTypeId; - public override string CreatorName - { - get { return _creatorName; } - } + public override string WriterName => _creatorName; - public override int WriterId - { - get { return _creatorId; } - } + public override string CreatorName => _creatorName; - public override int CreatorId - { - get { return _creatorId; } - } + public override int WriterId => _creatorId; - public override string Path - { - get { return _path; } - } + public override int CreatorId => _creatorId; - public override DateTime CreateDate - { - get { return _createDate; } - } + public override string Path => _path; - public override DateTime UpdateDate - { - get { return _updateDate; } - } + public override DateTime CreateDate => _createDate; - public override Guid Version - { - get { return Guid.Empty; } - } + public override DateTime UpdateDate => _updateDate; - public override int Level - { - get { return _level; } - } + public override Guid Version => Guid.Empty; - public override bool IsDraft - { - get { return false; } - } + public override int Level => _level; - public override ICollection Properties - { - get { return _properties; } - } + public override bool IsDraft => false; - public override IEnumerable Children - { - get { return _getChildren.Value; } - } + public override ICollection Properties => _properties; - public override IPublishedProperty GetProperty(string alias) + public override IEnumerable Children => _getChildren.Value; + + public override IPublishedProperty GetProperty(string alias) { return _getProperty(this, alias); } - public override PublishedContentType ContentType - { - get { return _contentType; } - } + public override PublishedContentType ContentType => _contentType; - // override to implement cache + // 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); - IPublishedProperty property; - string key = null; - var cache = UmbracoContextCache.Current; - - if (cache != null) - { - key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); - object o; - if (cache.TryGetValue(key, out o)) - { - property = o as IPublishedProperty; - if (property == null) - throw new InvalidOperationException("Corrupted cache."); - return property; - } - } - - // else get it for real, no cache - property = base.GetProperty(alias, true); - - if (cache != null) - cache[key] = property; - - return property; + 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 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; @@ -775,6 +754,34 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } + internal void Resync() + { + // clear recursive properties cached by XmlPublishedContent.GetProperty + // assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch) + // NOTE all properties cleared when clearing the content cache (see content cache) + //_cacheProvider.ClearCacheObjectTypes(); + //_cacheProvider.ClearCacheByKeySearch("XmlPublishedCache.PublishedMediaCache:RecursiveProperty-"); + } + + #region Content types + + public override PublishedContentType GetContentType(int id) + { + return _contentTypeCache.Get(PublishedItemType.Media, id); + } + + public override PublishedContentType GetContentType(string alias) + { + return _contentTypeCache.Get(PublishedItemType.Media, alias); + } + + public override IEnumerable GetByContentType(PublishedContentType contentType) + { + throw new NotImplementedException(); + } + + #endregion + // REFACTORING // caching the basic atomic values - and the parent id @@ -817,6 +824,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache parentId => parentId < 0 ? null : GetUmbracoMedia(parentId), GetChildrenMedia, GetProperty, + _cacheProvider, + _contentTypeCache, cacheValues.XPath, // though, outside of tests, that should be null cacheValues.FromExamine ); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMemberCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMemberCache.cs new file mode 100644 index 0000000000..2328a1cefd --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMemberCache.cs @@ -0,0 +1,148 @@ +using System; +using System.Text; +using System.Xml.XPath; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.Security; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PublishedMemberCache : IPublishedMemberCache + { + private readonly IMemberService _memberService; + private readonly ICacheProvider _requestCache; + private readonly XmlStore _xmlStore; + private readonly PublishedContentTypeCache _contentTypeCache; + + public PublishedMemberCache(XmlStore xmlStore, ICacheProvider requestCacheProvider, IMemberService memberService, PublishedContentTypeCache contentTypeCache) + { + _requestCache = requestCacheProvider; + _memberService = memberService; + _xmlStore = xmlStore; + _contentTypeCache = contentTypeCache; + } + + public IPublishedContent GetByProviderKey(object key) + { + return _requestCache.GetCacheItem( + GetCacheKey("GetByProviderKey", key), () => + { + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider() == false) + { + throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); + } + + var result = _memberService.GetByProviderKey(key); + if (result == null) return null; + var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); + return new PublishedMember(result, type).CreateModel(); + }); + } + + public IPublishedContent GetById(int memberId) + { + return _requestCache.GetCacheItem( + GetCacheKey("GetById", memberId), () => + { + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider() == false) + { + throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); + } + + var result = _memberService.GetById(memberId); + if (result == null) return null; + var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); + return new PublishedMember(result, type).CreateModel(); + }); + } + + public IPublishedContent GetByUsername(string username) + { + return _requestCache.GetCacheItem( + GetCacheKey("GetByUsername", username), () => + { + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider() == false) + { + throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); + } + + var result = _memberService.GetByUsername(username); + if (result == null) return null; + var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); + return new PublishedMember(result, type).CreateModel(); + }); + } + + public IPublishedContent GetByEmail(string email) + { + return _requestCache.GetCacheItem( + GetCacheKey("GetByEmail", email), () => + { + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider() == false) + { + throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); + } + + var result = _memberService.GetByEmail(email); + if (result == null) return null; + var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); + return new PublishedMember(result, type).CreateModel(); + }); + } + + public IPublishedContent GetByMember(IMember member) + { + var type = _contentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId); + return new PublishedMember(member, type).CreateModel(); + } + + public XPathNavigator CreateNavigator() + { + var doc = _xmlStore.GetMemberXml(); + return doc.CreateNavigator(); + } + + public XPathNavigator CreateNavigator(bool preview) + { + return CreateNavigator(); + } + + public XPathNavigator CreateNodeNavigator(int id, bool preview) + { + var n = _xmlStore.GetMemberXmlNode(id); + return n?.CreateNavigator(); + } + + private static string GetCacheKey(string key, params object[] additional) + { + var sb = new StringBuilder($"{typeof (MembershipHelper).Name}-{key}"); + foreach (var s in additional) + { + sb.Append("-"); + sb.Append(s); + } + return sb.ToString(); + } + + #region Content types + + public PublishedContentType GetContentType(int id) + { + return _contentTypeCache.Get(PublishedItemType.Member, id); + } + + public PublishedContentType GetContentType(string alias) + { + return _contentTypeCache.Get(PublishedItemType.Member, alias); + } + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs index f605d40686..e2d0846eb2 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs @@ -7,52 +7,28 @@ using Umbraco.Core.ObjectResolution; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { + // Note: RoutesCache closely follows the caching strategy dating from v4, which + // is obviously broken in many ways (eg it's a global cache but relying to some + // extend to the content cache, which itself is local to each request...). + // Not going to fix it anyway. + class RoutesCache { private ConcurrentDictionary _routes; private ConcurrentDictionary _nodeIds; + // NOTE + // RoutesCache is cleared by + // - ContentTypeCacheRefresher, whenever anything happens to any content type + // - DomainCacheRefresher, whenever anything happens to any domain + // - XmlStore, whenever anything happens to the XML cache + /// /// Initializes a new instance of the class. /// public RoutesCache() - : this(true) - { } - - /// - /// Initializes a new instance of the class. - /// - internal RoutesCache(bool bindToEvents) - { - Clear(); - - if (bindToEvents) - { - Resolution.Frozen += ResolutionFrozen; - } - } - - /// - /// Once resolution is frozen, then we can bind to the events that we require - /// - /// - /// - private void ResolutionFrozen(object s, EventArgs args) { - - // document - whenever a document is updated in, or removed from, the XML cache - // we must clear the cache - at the moment, we clear the entire cache - global::umbraco.content.AfterUpdateDocumentCache += (sender, e) => Clear(); - global::umbraco.content.AfterClearDocumentCache += (sender, e) => Clear(); - - // fixme - should refactor once content events are refactored - // the content class needs to be refactored - at the moment - // content.XmlContentInternal setter does not trigger any event - // content.UpdateDocumentCache(List Documents) does not trigger any event - // content.RefreshContentFromDatabaseAsync triggers AfterRefresh _while_ refreshing - // etc... - // in addition some events do not make sense... we trigger Publish when moving - // a node, which we should not (the node is moved, not published...) etc. + Clear(); } /// @@ -116,10 +92,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// The node identifier. public void ClearNode(int nodeId) { - if (!_routes.ContainsKey(nodeId)) return; + if (_routes.ContainsKey(nodeId) == false) return; string key; - if (!_routes.TryGetValue(nodeId, out key)) return; + if (_routes.TryGetValue(nodeId, out key) == false) return; int val; _nodeIds.TryRemove(key, out val); @@ -135,7 +111,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache _routes = new ConcurrentDictionary(); _nodeIds = new ConcurrentDictionary(); } - + #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs index 220338b635..6e021e5b9d 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs @@ -6,6 +6,7 @@ using System.Xml; using System.Xml.Serialization; using System.Xml.XPath; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; @@ -21,33 +22,41 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache [XmlType(Namespace = "http://umbraco.org/webservices/")] internal class XmlPublishedContent : PublishedContentWithKeyBase { - /// - /// Initializes a new instance of the XmlPublishedContent class with an Xml node. - /// - /// The Xml node. - /// A value indicating whether the published content is being previewed. - public XmlPublishedContent(XmlNode xmlNode, bool isPreviewing) + /// + /// Initializes a new instance of the XmlPublishedContent class with an Xml node. + /// + /// The Xml node. + /// A value indicating whether the published content is being previewed. + /// A cache provider. + /// A content type cache. + public XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache) { _xmlNode = xmlNode; _isPreviewing = isPreviewing; + _cacheProvider = cacheProvider; + _contentTypeCache = contentTypeCache; InitializeStructure(); Initialize(); InitializeChildren(); } - /// - /// Initializes a new instance of the XmlPublishedContent class with an Xml node, - /// and a value indicating whether to lazy-initialize the instance. - /// - /// The Xml node. - /// A value indicating whether the published content is being previewed. - /// A value indicating whether to lazy-initialize the instance. - /// Lazy-initializationg is NOT thread-safe. - internal XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, bool lazyInitialize) + /// + /// Initializes a new instance of the XmlPublishedContent class with an Xml node, + /// and a value indicating whether to lazy-initialize the instance. + /// + /// The Xml node. + /// A value indicating whether the published content is being previewed. + /// A cache provider. + /// A content type cache. + /// A value indicating whether to lazy-initialize the instance. + /// Lazy-initializationg is NOT thread-safe. + internal XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache, bool lazyInitialize) { _xmlNode = xmlNode; _isPreviewing = isPreviewing; - InitializeStructure(); + _cacheProvider = cacheProvider; + _contentTypeCache = contentTypeCache; + InitializeStructure(); if (lazyInitialize == false) { Initialize(); @@ -56,6 +65,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } private readonly XmlNode _xmlNode; + private readonly ICacheProvider _cacheProvider; + private readonly PublishedContentTypeCache _contentTypeCache; private bool _initialized; private bool _childrenInitialized; @@ -109,21 +120,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (recurse == false) return GetProperty(alias); - var cache = UmbracoContextCache.Current; + var key = string.Format("XmlPublishedCache.PublishedContentCache:RecursiveProperty-{0}-{1}", Id, alias.ToLowerInvariant()); + var cacheProvider = _cacheProvider; + return cacheProvider.GetCacheItem(key, () => base.GetProperty(alias, true)); - if (cache == null) - return base.GetProperty(alias, true); - - var key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); - var value = cache.GetOrAdd(key, k => base.GetProperty(alias, true)); - if (value == null) - return null; - - var property = value as IPublishedProperty; - if (property == null) - throw new InvalidOperationException("Corrupted cache."); - - return property; + // note: cleared by PublishedContentCache.Resync - any change here must be applied there } public override PublishedItemType ItemType @@ -349,7 +350,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (parent == null) return; if (parent.Attributes != null && parent.Attributes.GetNamedItem("isDoc") != null) - _parent = (new XmlPublishedContent(parent, _isPreviewing, true)).CreateModel(); + _parent = (new XmlPublishedContent(parent, _isPreviewing, _cacheProvider, _contentTypeCache, true)).CreateModel(); } private void Initialize() @@ -409,7 +410,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var dataXPath = "* [not(@isDoc)]"; var nodes = _xmlNode.SelectNodes(dataXPath); - _contentType = PublishedContentType.Get(PublishedItemType.Content, _docTypeAlias); + _contentType = _contentTypeCache.Get(PublishedItemType.Content, _docTypeAlias); var propertyNodes = new Dictionary(); if (nodes != null) @@ -443,7 +444,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var iterator = nav.Select(expr); while (iterator.MoveNext()) _children.Add( - (new XmlPublishedContent(((IHasXmlNode)iterator.Current).GetNode(), _isPreviewing, true)).CreateModel()); + (new XmlPublishedContent(((IHasXmlNode)iterator.Current).GetNode(), _isPreviewing, _cacheProvider, _contentTypeCache, true)).CreateModel()); // warn: this is not thread-safe _childrenInitialized = true; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs new file mode 100644 index 0000000000..81a44cfd88 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs @@ -0,0 +1,2178 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using NPoco; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Core.Strings; +using Umbraco.Core.Xml; +using Umbraco.Web.Cache; +using Umbraco.Web.Scheduling; +using Content = umbraco.cms.businesslogic.Content; +using File = System.IO.File; +using Task = System.Threading.Tasks.Task; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// Represents the Xml storage for the Xml published cache. + /// + /// + /// One instance of is instanciated by the and + /// then passed to all instances that are created (one per request). + /// This class should *not* be public. + /// + class XmlStore : IDisposable + { + private Func _xmlContentSerializer; + private Func _xmlMemberSerializer; + private Func _xmlMediaSerializer; + private XmlStoreFilePersister _persisterTask; + private volatile bool _released; + private bool _withRepositoryEvents; + private bool _withOtherEvents; + + private readonly PublishedContentTypeCache _contentTypeCache; + private readonly RoutesCache _routesCache; + private readonly ServiceContext _serviceContext; // fixme WHY + private readonly IDatabaseUnitOfWorkProvider _uowProvider; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The default constructor will boot the cache, load data from file or database, + /// wire events in order to manage changes, etc. + public XmlStore(ServiceContext serviceContext, IDatabaseUnitOfWorkProvider uowProvider, RoutesCache routesCache, PublishedContentTypeCache contentTypeCache, IEnumerable segmentProviders) + : this(serviceContext, uowProvider, routesCache, contentTypeCache, segmentProviders, false, false) + { } + + // internal for unit tests + // no file nor db, no config check + // fixme - er, we DO have a DB? + internal XmlStore(ServiceContext serviceContext, IDatabaseUnitOfWorkProvider uowProvider, RoutesCache routesCache, PublishedContentTypeCache contentTypeCache, + IEnumerable segmentProviders, bool testing, bool enableRepositoryEvents) + { + if (testing == false) + EnsureConfigurationIsValid(); + + _serviceContext = serviceContext; + _uowProvider = uowProvider; + _routesCache = routesCache; + _contentTypeCache = contentTypeCache; + + InitializeSerializers(segmentProviders); + + if (testing) + { + _xmlFileEnabled = false; + } + else + { + InitializeFilePersister(); + } + + // need to wait for resolution to be frozen + if (Resolution.IsFrozen) + OnResolutionFrozen(testing, enableRepositoryEvents); + else + Resolution.Frozen += (sender, args) => OnResolutionFrozen(testing, enableRepositoryEvents); + } + + // internal for unit tests + // initialize with an xml document + // no events, no file nor db, no config check + internal XmlStore(XmlDocument xmlDocument) + { + _xmlDocument = xmlDocument; + _xmlFileEnabled = false; + + // do not plug events, we may not have what it takes to handle them + } + + // internal for unit tests + // initialize with a function returning an xml document + // no events, no file nor db, no config check + internal XmlStore(Func getXmlDocument) + { + if (getXmlDocument == null) + throw new ArgumentNullException(nameof(getXmlDocument)); + GetXmlDocument = getXmlDocument; + _xmlFileEnabled = false; + + // do not plug events, we may not have what it takes to handle them + } + + private void InitializeSerializers(IEnumerable segmentProviders) + { + var exs = new EntityXmlSerializer(); + _xmlContentSerializer = c => exs.Serialize(_serviceContext.ContentService, _serviceContext.DataTypeService, _serviceContext.UserService, segmentProviders, c); + _xmlMemberSerializer = m => exs.Serialize(_serviceContext.DataTypeService, m); + _xmlMediaSerializer = m => exs.Serialize(_serviceContext.MediaService, _serviceContext.DataTypeService, _serviceContext.UserService, segmentProviders, m); + } + + private void InitializeFilePersister() + { + if (SyncToXmlFile == false) return; + + var logger = LoggerResolver.Current.Logger; + + // there's always be one task keeping a ref to the runner + // so it's safe to just create it as a local var here + var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions + { + LongRunning = true, + KeepAlive = true, + Hosted = false // main domain will take care of stopping the runner (see below) + }, logger); + + // create (and add to runner) + _persisterTask = new XmlStoreFilePersister(runner, this, logger); + + var registered = ApplicationContext.Current.MainDom.Register( + null, + () => + { + // once released, the cache still works but does not write to file anymore, + // which is OK with database server messenger but will cause data loss with + // another messenger... + + runner.Shutdown(false, true); // wait until flushed + _persisterTask = null; // fail fast + _released = true; + }); + + // failed to become the main domain, we will never use the file + if (registered == false) + runner.Shutdown(false, true); + + _released = (registered == false); + } + + private void OnResolutionFrozen(bool testing, bool enableRepositoryEvents) + { + if (testing == false || enableRepositoryEvents) + InitializeRepositoryEvents(); + if (testing) + return; + InitializeOtherEvents(); + InitializeContent(); + } + + private void InitializeRepositoryEvents() + { + // plug repository event handlers + // these trigger within the transaction to ensure consistency + // and are used to maintain the central, database-level XML cache + ContentRepository.UowRemovingEntity += OnContentRemovingEntity; + ContentRepository.UowRemovingVersion += OnContentRemovingVersion; + ContentRepository.UowRefreshedEntity += OnContentRefreshedEntity; + MediaRepository.UowRemovingEntity += OnMediaRemovingEntity; + MediaRepository.UowRemovingVersion += OnMediaRemovingVersion; + MediaRepository.UowRefreshedEntity += OnMediaRefreshedEntity; + MemberRepository.UowRemovingEntity += OnMemberRemovingEntity; + MemberRepository.UowRemovingVersion += OnMemberRemovingVersion; + MemberRepository.UowRefreshedEntity += OnMemberRefreshedEntity; + + // plug + ContentTypeService.UowRefreshedEntity += OnContentTypeRefreshedEntity; + MediaTypeService.UowRefreshedEntity += OnMediaTypeRefreshedEntity; + MemberTypeService.UowRefreshedEntity += OnMemberTypeRefreshedEntity; + + _withRepositoryEvents = true; + } + + private void InitializeOtherEvents() + { + // temp - until we get rid of Content + Content.DeletedContent += OnDeletedContent; + + _withOtherEvents = true; + } + + private void ClearEvents() + { + ContentRepository.UowRemovingEntity -= OnContentRemovingEntity; + ContentRepository.UowRemovingVersion -= OnContentRemovingVersion; + ContentRepository.UowRefreshedEntity -= OnContentRefreshedEntity; + MediaRepository.UowRemovingEntity -= OnMediaRemovingEntity; + MediaRepository.UowRemovingVersion -= OnMediaRemovingVersion; + MediaRepository.UowRefreshedEntity -= OnMediaRefreshedEntity; + MemberRepository.UowRemovingEntity -= OnMemberRemovingEntity; + MemberRepository.UowRemovingVersion -= OnMemberRemovingVersion; + MemberRepository.UowRefreshedEntity -= OnMemberRefreshedEntity; + + ContentTypeService.UowRefreshedEntity -= OnContentTypeRefreshedEntity; + MediaTypeService.UowRefreshedEntity -= OnMediaTypeRefreshedEntity; + MemberTypeService.UowRefreshedEntity -= OnMemberTypeRefreshedEntity; + + Content.DeletedContent -= OnDeletedContent; + + _withRepositoryEvents = false; + _withOtherEvents = false; + } + + private void InitializeContent() + { + // and populate the cache + using (var safeXml = GetSafeXmlWriter(false)) + { + bool registerXmlChange; + LoadXmlLocked(safeXml, out registerXmlChange); + safeXml.Commit(registerXmlChange); + } + } + + public void Dispose() + { + ClearEvents(); + } + + #endregion + + #region Configuration + + // gathering configuration options here to document what they mean + + private readonly bool _xmlFileEnabled = true; + + // whether the disk cache is enabled + private bool XmlFileEnabled => _xmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled; + + // whether the disk cache is enabled and to update the disk cache when xml changes + private bool SyncToXmlFile => XmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache; + + // whether the disk cache is enabled and to reload from disk cache if it changes + private bool SyncFromXmlFile => XmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.XmlContentCheckForDiskChanges; + + // whether _xml is immutable or not (achieved by cloning before changing anything) + private static bool XmlIsImmutable => UmbracoConfig.For.UmbracoSettings().Content.CloneXmlContent; + + // whether to keep version of everything (incl. medias & members) in cmsPreviewXml + // for audit purposes - false by default, not in umbracoSettings.config + // whether to... no idea what that one does + // it is false by default and not in UmbracoSettings.config anymore - ignoring + /* + private static bool GlobalPreviewStorageEnabled + { + get { return UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled; } + } + */ + + // ensures config is valid + private void EnsureConfigurationIsValid() + { + if (SyncToXmlFile && SyncFromXmlFile) + throw new Exception("Cannot run with both ContinouslyUpdateXmlDiskCache and XmlContentCheckForDiskChanges being true."); + + if (XmlIsImmutable == false) + //LogHelper.Warn("Running with CloneXmlContent being false is a bad idea."); + LogHelper.Warn("CloneXmlContent is false - ignored, we always clone."); + + // note: if SyncFromXmlFile then we should also disable / warn that local edits are going to cause issues... + } + + #endregion + + #region Xml + + /// + /// Gets or sets the delegate used to retrieve the Xml content, used for unit tests, else should + /// be null and then the default content will be used. For non-preview content only. + /// + /// + /// The default content ONLY works when in the context an Http Request mostly because the + /// 'content' object heavily relies on HttpContext, SQL connections and a bunch of other stuff + /// that when run inside of a unit test fails. + /// + public Func GetXmlDocument { get; set; } + + private readonly XmlDocument _xmlDocument; // supplied xml document (for tests) + private volatile XmlDocument _xml; // master xml document + private readonly AsyncLock _xmlLock = new AsyncLock(); // protects _xml + + // to be used by PublishedContentCache only + // for non-preview content only + public XmlDocument Xml + { + get + { + if (_xmlDocument != null) + return _xmlDocument; + if (GetXmlDocument != null) + return GetXmlDocument(); + + ReloadXmlFromFileIfChanged(); + return _xml; + } + } + + // assumes xml lock + private void SetXmlLocked(XmlDocument xml, bool registerXmlChange) + { + // this is the ONLY place where we write to _xml + _xml = xml; + + if (_routesCache != null) + _routesCache.Clear(); // anytime we set _xml + + if (registerXmlChange == false || SyncToXmlFile == false) + return; + + if (_persisterTask != null) + _persisterTask = _persisterTask.Touch(); + } + + private static XmlDocument Clone(XmlDocument xmlDoc) + { + return xmlDoc == null ? null : (XmlDocument)xmlDoc.CloneNode(true); + } + + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) + { + string subset = null; + + // get current doctype + var n = xml.FirstChild; + while (n.NodeType != XmlNodeType.DocumentType && n.NextSibling != null) + n = n.NextSibling; + if (n.NodeType == XmlNodeType.DocumentType) + subset = ((XmlDocumentType)n).InternalSubset; + + // ensure it contains the content type + if (subset != null && subset.Contains(string.Format("", contentTypeAlias))) + return xml; + + // alas, that does not work, replacing a doctype is ignored and GetElementById fails + // + //// remove current doctype, set new doctype + //xml.RemoveChild(n); + //subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); + //var doctype = xml.CreateDocumentType("root", null, null, subset); + //xml.InsertAfter(doctype, xml.FirstChild); + + var xml2 = new XmlDocument(); + subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); + var doctype = xml2.CreateDocumentType("root", null, null, subset); + xml2.AppendChild(doctype); + xml2.AppendChild(xml2.ImportNode(xml.DocumentElement, true)); + return xml2; + } + + private static void InitializeXml(XmlDocument xml, string dtd) + { + // prime the xml document with an inline dtd and a root element + xml.LoadXml(String.Format("{0}{1}{0}", Environment.NewLine, dtd)); + } + + private static void PopulateXml(IDictionary> hierarchy, IDictionary nodeIndex, int parentId, XmlNode parentNode) + { + List children; + if (hierarchy.TryGetValue(parentId, out children) == false) return; + + foreach (var childId in children) + { + // append child node to parent node and recursively take care of the child + var childNode = nodeIndex[childId]; + parentNode.AppendChild(childNode); + PopulateXml(hierarchy, nodeIndex, childId, childNode); + } + } + + /// + /// Generates the complete (simplified) XML DTD. + /// + /// The DTD as a string + private string GetDtd() + { + var dtd = new StringBuilder(); + dtd.AppendLine(" x.Alias.ToSafeAlias()).WhereNotNull(); + foreach (var alias in aliases) + { + dtdInner.AppendLine($""); + dtdInner.AppendLine($""); + } + dtd.Append(dtdInner); + } + catch (Exception exception) + { + LogHelper.Error("Failed to build a DTD for the Xml cache.", exception); + } + + dtd.AppendLine("]>"); + return dtd.ToString(); + } + + // try to load from file, otherwise database + // assumes xml lock (file is always locked) + private void LoadXmlLocked(SafeXmlReaderWriter safeXml, out bool registerXmlChange) + { + LogHelper.Debug("Loading Xml..."); + + // try to get it from the file + if (XmlFileEnabled && (safeXml.Xml = LoadXmlFromFile()) != null) + { + registerXmlChange = false; // loaded from disk, do NOT write back to disk! + return; + } + + // get it from the database, and register + LoadXmlTreeFromDatabaseLocked(safeXml); + registerXmlChange = true; + } + + public XmlNode GetMediaXmlNode(int mediaId) + { + // there's only one version for medias + + const string sql = @"SELECT umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.Level, +cmsContentXml.xml, 1 AS published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType +AND (umbracoNode.id=@id)"; + + XmlDto xmlDto; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.MediaTree); + var xmlDtos = uow.Database.Query(sql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Media), + @id = mediaId + }); + xmlDto = xmlDtos.FirstOrDefault(); + uow.Complete(); + } + + if (xmlDto == null) return null; + + var doc = new XmlDocument(); + var xml = doc.ReadNode(XmlReader.Create(new StringReader(xmlDto.Xml))); + return xml; + } + + public XmlNode GetMemberXmlNode(int memberId) + { + // there's only one version for members + + const string sql = @"SELECT umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.Level, +cmsContentXml.xml, 1 AS published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType +AND (umbracoNode.id=@id)"; + + XmlDto xmlDto; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.MemberTree); + var xmlDtos = uow.Database.Query(sql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Member), + @id = memberId + }); + xmlDto = xmlDtos.FirstOrDefault(); + uow.Complete(); + } + + if (xmlDto == null) return null; + + var doc = new XmlDocument(); + var xml = doc.ReadNode(XmlReader.Create(new StringReader(xmlDto.Xml))); + return xml; + } + + public XmlNode GetPreviewXmlNode(int contentId) + { + const string sql = @"SELECT umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.Level, +cmsPreviewXml.xml, cmsDocument.published +FROM umbracoNode +JOIN cmsPreviewXml ON (cmsPreviewXml.nodeId=umbracoNode.id) +JOIN cmsDocument ON (cmsDocument.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType AND cmsDocument.newest=1 +AND (umbracoNode.id=@id)"; + + XmlDto xmlDto; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var xmlDtos = uow.Database.Query(sql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Document), + @id = contentId + }); + xmlDto = xmlDtos.FirstOrDefault(); + uow.Complete(); + } + if (xmlDto == null) return null; + + var doc = new XmlDocument(); + var xml = doc.ReadNode(XmlReader.Create(new StringReader(xmlDto.Xml))); + if (xml == null || xml.Attributes == null) return null; + + if (xmlDto.Published == false) + xml.Attributes.Append(doc.CreateAttribute("isDraft")); + return xml; + } + + public XmlDocument GetMediaXml() + { + // this is not efficient at all, not cached, nothing + // just here to replicate what uQuery was doing and show it can be done + // but really - should not be used + + return LoadMoreXmlFromDatabase(new Guid(Constants.ObjectTypes.Media)); + } + + public XmlDocument GetMemberXml() + { + // this is not efficient at all, not cached, nothing + // just here to replicate what uQuery was doing and show it can be done + // but really - should not be used + + return LoadMoreXmlFromDatabase(new Guid(Constants.ObjectTypes.Member)); + } + + public XmlDocument GetPreviewXml(int contentId, bool includeSubs) + { + var content = _serviceContext.ContentService.GetById(contentId); + + var doc = (XmlDocument)Xml.Clone(); + if (content == null) return doc; + + IEnumerable xmlDtos; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var sqlSyntax = uow.Database.SqlSyntax; + var sql = ReadCmsPreviewXmlSql1; + sql += " @path LIKE " + sqlSyntax.GetConcat("umbracoNode.Path", "',%"); // concat(umbracoNode.path, ',%') + if (includeSubs) sql += " OR umbracoNode.path LIKE " + sqlSyntax.GetConcat("@path", "',%"); // concat(@path, ',%') + sql += ReadCmsPreviewXmlSql2; + xmlDtos = uow.Database.Query(sql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Document), + @path = content.Path, + }).ToArray(); // todo - do we want to load them all? + uow.Complete(); + } + + foreach (var xmlDto in xmlDtos) + { + var xml = xmlDto.XmlNode = doc.ReadNode(XmlReader.Create(new StringReader(xmlDto.Xml))); + if (xml == null || xml.Attributes == null) continue; + if (xmlDto.Published == false) + xml.Attributes.Append(doc.CreateAttribute("isDraft")); + doc = AddOrUpdateXmlNode(doc, xmlDto); + } + + return doc; + } + + // NOTE + // - this is NOT a reader/writer lock and each lock is exclusive + // - these locks are NOT reentrant / recursive + + // gets a locked safe read access to the main xml + private SafeXmlReaderWriter GetSafeXmlReader() + { + var releaser = _xmlLock.Lock(); + return SafeXmlReaderWriter.GetReader(this, releaser); + } + + // gets a locked safe read accses to the main xml + private async Task GetSafeXmlReaderAsync() + { + var releaser = await _xmlLock.LockAsync(); + return SafeXmlReaderWriter.GetReader(this, releaser); + } + + // gets a locked safe write access to the main xml (cloned) + private SafeXmlReaderWriter GetSafeXmlWriter(bool auto = true) + { + var releaser = _xmlLock.Lock(); + return SafeXmlReaderWriter.GetWriter(this, releaser, auto); + } + + private class SafeXmlReaderWriter : IDisposable + { + private readonly XmlStore _store; + private IDisposable _releaser; + private bool _isWriter; + private bool _auto; + private bool _committed; + private XmlDocument _xml; + + private SafeXmlReaderWriter(XmlStore store, IDisposable releaser, bool isWriter, bool auto) + { + _store = store; + _releaser = releaser; + _isWriter = isWriter; + _auto = auto; + + // cloning for writer is not an option anymore (see XmlIsImmutable) + _xml = _isWriter ? Clone(store._xml) : store._xml; + } + + public static SafeXmlReaderWriter GetReader(XmlStore store, IDisposable releaser) + { + return new SafeXmlReaderWriter(store, releaser, false, false); + } + + public static SafeXmlReaderWriter GetWriter(XmlStore store, IDisposable releaser, bool auto) + { + return new SafeXmlReaderWriter(store, releaser, true, auto); + } + + public void UpgradeToWriter(bool auto) + { + if (_isWriter) + throw new InvalidOperationException("Already writing."); + _isWriter = true; + _auto = auto; + _xml = Clone(_xml); // cloning for writer is not an option anymore (see XmlIsImmutable) + } + + public XmlDocument Xml + { + get + { + return _xml; + } + set + { + if (_isWriter == false) + throw new InvalidOperationException("Not writing."); + _xml = value; + } + } + + // registerXmlChange indicates whether to do what should be done when Xml changes, + // that is, to request that the file be written to disk - something we don't want + // to do if we're committing Xml precisely after we've read from disk! + public void Commit(bool registerXmlChange = true) + { + if (_isWriter == false) + throw new InvalidOperationException("Not writing."); + _store.SetXmlLocked(_xml, registerXmlChange); + _committed = true; + } + + public void Dispose() + { + if (_releaser == null) + return; + if (_isWriter && _auto && _committed == false) + Commit(); + _releaser.Dispose(); + _releaser = null; + } + } + + private const string ChildNodesXPath = "./* [@id]"; + private const string DataNodesXPath = "./* [not(@id)]"; + + #endregion + + #region File + + private readonly string _xmlFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); + private DateTime _lastFileRead; // last time the file was read + private DateTime _nextFileCheck; // last time we checked whether the file was changed + + public void EnsureFilePermission() + { + // FIXME - but do we really have a store, initialized, at that point? + var filename = _xmlFileName + ".temp"; + File.WriteAllText(filename, "TEMP"); + File.Delete(filename); + } + + // not used - just try to read the file + //private bool XmlFileExists + //{ + // get + // { + // // check that the file exists and has content (is not empty) + // var fileInfo = new FileInfo(_xmlFileName); + // return fileInfo.Exists && fileInfo.Length > 0; + // } + //} + + private DateTime XmlFileLastWriteTime + { + get + { + var fileInfo = new FileInfo(_xmlFileName); + return fileInfo.Exists ? fileInfo.LastWriteTimeUtc : DateTime.MinValue; + } + } + + // invoked by XmlStoreFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) + internal void SaveXmlToFile() + { + LogHelper.Info("Save Xml to file..."); + + try + { + var xml = _xml; // capture (atomic + volatile), immutable anyway + if (xml == null) return; + + // delete existing file, if any + DeleteXmlFile(); + + // ensure cache directory exists + var directoryName = Path.GetDirectoryName(_xmlFileName); + if (directoryName == null) + throw new Exception($"Invalid XmlFileName \"{_xmlFileName}\"."); + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + Directory.CreateDirectory(directoryName); + + // save + using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) + { + var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); + fs.Write(bytes, 0, bytes.Length); + } + + LogHelper.Info("Saved Xml to file."); + } + catch (Exception e) + { + // if something goes wrong remove the file + DeleteXmlFile(); + + LogHelper.Error("Failed to save Xml to file.", e); + } + } + + // invoked by XmlStoreFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) + internal async Task SaveXmlToFileAsync() + { + LogHelper.Info("Save Xml to file..."); + + try + { + var xml = _xml; // capture (atomic + volatile), immutable anyway + if (xml == null) return; + + // delete existing file, if any + DeleteXmlFile(); + + // ensure cache directory exists + var directoryName = Path.GetDirectoryName(_xmlFileName); + if (directoryName == null) + throw new Exception($"Invalid XmlFileName \"{_xmlFileName}\"."); + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + Directory.CreateDirectory(directoryName); + + // save + using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) + { + var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); + await fs.WriteAsync(bytes, 0, bytes.Length); + } + + LogHelper.Info("Saved Xml to file."); + } + catch (Exception e) + { + // if something goes wrong remove the file + DeleteXmlFile(); + + LogHelper.Error("Failed to save Xml to file.", e); + } + } + + private string SaveXmlToString(XmlDocument xml) + { + // using that one method because we want to have proper indent + // and in addition, writing async is never fully async because + // althouth the writer is async, xml.WriteTo() will not async + + // that one almost works but... "The elements are indented as long as the element + // does not contain mixed content. Once the WriteString or WriteWhitespace method + // is called to write out a mixed element content, the XmlWriter stops indenting. + // The indenting resumes once the mixed content element is closed." - says MSDN + // about XmlWriterSettings.Indent + + // so ImportContent must also make sure of ignoring whitespaces! + + var sb = new StringBuilder(); + using (var xmlWriter = XmlWriter.Create(sb, new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + //OmitXmlDeclaration = true + })) + { + //xmlWriter.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); + xml.WriteTo(xmlWriter); // already contains the xml declaration + } + return sb.ToString(); + } + + private XmlDocument LoadXmlFromFile() + { + // do NOT try to load if we are not the main domain anymore + if (_released) return null; + + LogHelper.Info("Load Xml from file..."); + + try + { + var xml = new XmlDocument(); + using (var fs = new FileStream(_xmlFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + xml.Load(fs); + } + _lastFileRead = DateTime.UtcNow; + LogHelper.Info("Loaded Xml from file."); + return xml; + } + catch (FileNotFoundException) + { + LogHelper.Warn("Failed to load Xml, file does not exist."); + return null; + } + catch (Exception e) + { + LogHelper.Error("Failed to load Xml from file.", e); + DeleteXmlFile(); + return null; + } + } + + private void DeleteXmlFile() + { + if (File.Exists(_xmlFileName) == false) return; + File.SetAttributes(_xmlFileName, FileAttributes.Normal); + File.Delete(_xmlFileName); + } + + private void ReloadXmlFromFileIfChanged() + { + if (SyncFromXmlFile == false) return; + + var now = DateTime.UtcNow; + if (now < _nextFileCheck) return; + + // time to check + _nextFileCheck = now.AddSeconds(1); // check every 1s + if (XmlFileLastWriteTime <= _lastFileRead) return; + + LogHelper.Debug("Xml file change detected, reloading."); + + // time to read + + using (var safeXml = GetSafeXmlWriter(false)) + { + bool registerXmlChange; + LoadXmlLocked(safeXml, out registerXmlChange); // updates _lastFileRead + safeXml.Commit(registerXmlChange); + } + } + + #endregion + + #region Database + + const string ReadTreeCmsContentXmlSql = @"SELECT + umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.level, umbracoNode.path, + cmsContentXml.xml, cmsContentXml.rv, cmsDocument.published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +JOIN cmsDocument ON (cmsDocument.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType AND cmsDocument.published=1 +ORDER BY umbracoNode.level, umbracoNode.sortOrder"; + + const string ReadBranchCmsContentXmlSql = @"SELECT + umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.level, umbracoNode.path, + cmsContentXml.xml, cmsContentXml.rv, cmsDocument.published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +JOIN cmsDocument ON (cmsDocument.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType AND cmsDocument.published=1 AND (umbracoNode.id = @id OR umbracoNode.path LIKE @path) +ORDER BY umbracoNode.level, umbracoNode.sortOrder"; + + const string ReadCmsContentXmlForContentTypesSql = @"SELECT + umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.level, umbracoNode.path, + cmsContentXml.xml, cmsContentXml.rv, cmsDocument.published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +JOIN cmsDocument ON (cmsDocument.nodeId=umbracoNode.id) +JOIN cmsContent ON (cmsDocument.nodeId=cmsContent.nodeId) +WHERE umbracoNode.nodeObjectType = @nodeObjectType AND cmsDocument.published=1 AND cmsContent.contentType IN (@ids) +ORDER BY umbracoNode.level, umbracoNode.sortOrder"; + + const string ReadMoreCmsContentXmlSql = @"SELECT + umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.level, umbracoNode.path, + cmsContentXml.xml, cmsContentXml.rv, 1 AS published +FROM umbracoNode +JOIN cmsContentXml ON (cmsContentXml.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType +ORDER BY umbracoNode.level, umbracoNode.sortOrder"; + + private const string ReadCmsPreviewXmlSql1 = @"SELECT + umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, umbracoNode.level, umbracoNode.path, + cmsPreviewXml.xml, cmsPreviewXml.rv, cmsDocument.published +FROM umbracoNode +JOIN cmsPreviewXml ON (cmsPreviewXml.nodeId=umbracoNode.id) +JOIN cmsDocument ON (cmsDocument.nodeId=umbracoNode.id) +WHERE umbracoNode.nodeObjectType = @nodeObjectType AND cmsDocument.newest=1 +AND (umbracoNode.path=@path OR"; // @path LIKE concat(umbracoNode.path, ',%')"; + const string ReadCmsPreviewXmlSql2 = @") +ORDER BY umbracoNode.level, umbracoNode.sortOrder"; + + // ReSharper disable once ClassNeverInstantiated.Local + private class XmlDto + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + + public int Id { get; set; } + public long Rv { get; set; } + public int ParentId { get; set; } + //public int SortOrder { get; set; } + public int Level { get; set; } + public string Path { get; set; } + public string Xml { get; set; } + public bool Published { get; set; } + + [Ignore] + public XmlNode XmlNode { get; set; } + + // ReSharper restore UnusedAutoPropertyAccessor.Local + } + + // assumes xml lock + private void LoadXmlTreeFromDatabaseLocked(SafeXmlReaderWriter safeXml) + { + // initialise the document ready for the composition of content + var xml = new XmlDocument(); + InitializeXml(xml, GetDtd()); + + var parents = new Dictionary(); + + var dtoQuery = LoadXmlTreeDtoFromDatabaseLocked(xml); + foreach (var dto in dtoQuery) + { + XmlNode parent; + if (parents.TryGetValue(dto.ParentId, out parent) == false) + { + parent = dto.ParentId == -1 + ? xml.DocumentElement + : xml.GetElementById(dto.ParentId.ToInvariantString()); + + if (parent == null) + continue; + + parents[dto.ParentId] = parent; + } + + parent.AppendChild(dto.XmlNode); + } + + safeXml.Xml = xml; + } + + // assumes xml lock + private IEnumerable LoadXmlTreeDtoFromDatabaseLocked(XmlDocument xmlDoc) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + + // get xml + var xmlDtos = uow.Database.Query(ReadTreeCmsContentXmlSql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Document) + }); + + // create nodes + var nodes = xmlDtos.Select(x => + { + // parse into a DOM node + x.XmlNode = ImportContent(xmlDoc, x); + return x; + }).ToArray(); // todo - could move uow mgt up to avoid ToArray here + + uow.Complete(); + return nodes; + } + } + + // assumes xml lock + private IEnumerable LoadXmlBranchDtoFromDatabaseLocked(XmlDocument xmlDoc, int id, string path) + { + // get xml + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + + var xmlDtos = uow.Database.Query(ReadBranchCmsContentXmlSql, + new + { + @nodeObjectType = new Guid(Constants.ObjectTypes.Document), + @path = path + ",%", + /*@id =*/ + id + }); + + // create nodes + var nodes = xmlDtos.Select(x => + { + // parse into a DOM node + x.XmlNode = ImportContent(xmlDoc, x); + return x; + }).ToArray(); // todo - could move uow mgt up to avoid ToArray here + + uow.Complete(); + return nodes; + } + } + + private XmlDocument LoadMoreXmlFromDatabase(Guid nodeObjectType) + { + // FIXME - has been optimized in 7.4 - see how! + var hierarchy = new Dictionary>(); + var nodeIndex = new Dictionary(); + + var xmlDoc = new XmlDocument(); + + using (var uow = _uowProvider.CreateUnitOfWork()) + { + if (nodeObjectType == Constants.ObjectTypes.DocumentGuid) + uow.ReadLock(Constants.Locks.ContentTree); + else if (nodeObjectType == Constants.ObjectTypes.MediaGuid) + uow.ReadLock(Constants.Locks.MediaTree); + else if (nodeObjectType == Constants.ObjectTypes.MemberGuid) + uow.ReadLock(Constants.Locks.MemberTree); + + var xmlDtos = uow.Database.Query(ReadMoreCmsContentXmlSql, + new { /*@nodeObjectType =*/ nodeObjectType }); + + foreach (var xmlDto in xmlDtos) + { + // and parse it into a DOM node + xmlDoc.LoadXml(xmlDto.Xml); + var node = xmlDoc.FirstChild; + nodeIndex.Add(xmlDto.Id, node); + + // Build the content hierarchy + List children; + if (hierarchy.TryGetValue(xmlDto.ParentId, out children) == false) + { + // No children for this parent, so add one + children = new List(); + hierarchy.Add(xmlDto.ParentId, children); + } + children.Add(xmlDto.Id); + } + + // If we got to here we must have successfully retrieved the content from the DB so + // we can safely initialise and compose the final content DOM. + // Note: We are reusing the XmlDocument used to create the xml nodes above so + // we don't have to import them into a new XmlDocument + + // Initialise the document ready for the final composition of content + InitializeXml(xmlDoc, string.Empty); + + // Start building the content tree recursively from the root (-1) node + PopulateXml(hierarchy, nodeIndex, -1, xmlDoc.DocumentElement); + + uow.Complete(); + } + + return xmlDoc; + } + + // internal - used by umbraco.content.RefreshContentFromDatabase[Async] + internal void ReloadXmlFromDatabase() + { + // event - cancel + + // nobody should work on the Xml while we load + using (var safeXml = GetSafeXmlWriter()) + { + LoadXmlTreeFromDatabaseLocked(safeXml); + } + } + + #endregion + + #region Handle Distributed Notifications for Memory Xml + + // NOT using events, see notes in IPublishedCachesService + + public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) + { + draftChanged = true; // by default - we don't track drafts + publishedChanged = false; + + if (_withOtherEvents == false) + return; + + // process all changes on one xml clone + using (var safeXml = GetSafeXmlWriter(false)) // not auto-commit + { + foreach (var payload in payloads) + { + LogHelper.Debug($"Notified {payload.ChangeTypes} for content {payload.Id}."); + + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) + { + LoadXmlTreeFromDatabaseLocked(safeXml); + publishedChanged = true; + continue; + } + + if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) + { + var toRemove = safeXml.Xml.GetElementById(payload.Id.ToInvariantString()); + if (toRemove != null) + { + if (toRemove.ParentNode == null) throw new Exception("oops"); + toRemove.ParentNode.RemoveChild(toRemove); + publishedChanged = true; + } + continue; + } + + if (payload.ChangeTypes.HasTypesNone(TreeChangeTypes.RefreshNode | TreeChangeTypes.RefreshBranch)) + { + // ?! + continue; + } + + var content = _serviceContext.ContentService.GetById(payload.Id); + var current = safeXml.Xml.GetElementById(payload.Id.ToInvariantString()); + + if (content == null || content.HasPublishedVersion == false || content.Trashed) + { + // no published version + LogHelper.Debug($"Notified, content {payload.Id} has no published version."); + if (current != null) + { + // remove from xml if exists + if (current.ParentNode == null) throw new Exception("oops"); + current.ParentNode.RemoveChild(current); + publishedChanged = true; + } + + continue; + } + + // else we have a published version + + // that query is yielding results so will only load what's needed + // + // 'using' the enumerator ensures that the enumeration is properly terminated even if abandonned + // otherwise, it would leak an open reader & an un-released database connection + // see PetaPoco.Query(Type[] types, Delegate cb, string sql, params object[] args) + // and read http://blogs.msdn.com/b/oldnewthing/archive/2008/08/14/8862242.aspx + var dtoQuery = LoadXmlBranchDtoFromDatabaseLocked(safeXml.Xml, content.Id, content.Path); + using (var dtos = dtoQuery.GetEnumerator()) + { + if (dtos.MoveNext() == false) + { + // gone fishing, remove (possible race condition) + LogHelper.Debug($"Notifified, content {payload.Id} gone fishing."); + if (current != null) + { + // remove from xml if exists + if (current.ParentNode == null) throw new Exception("oops"); + current.ParentNode.RemoveChild(current); + publishedChanged = true; + } + continue; + } + + if (dtos.Current.Id != content.Id) + throw new Exception("oops"); // first one should be 'current' + var currentDto = dtos.Current; + + // note: if anything eg parentId or path or level has changed, then rv has changed too + var currentRv = current == null ? -1 : int.Parse(current.Attributes["rv"].Value); + + // if exists and unchanged and not refreshing the branch, skip entirely + if (current != null + && currentRv == currentDto.Rv + && payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch) == false) + continue; + + // note: Examine would not be able to do the path trick below, and we cannot help for + // unpublished content, so it *is* possible that Examine is inconsistent for a while, + // though events should get it consistent eventually. + + // note: if path has changed we must do a branch refresh, even if the event is not requiring + // it, otherwise we would update the local node and not its children, who would then have + // inconsistent level (and path) attributes. + + var refreshBranch = current == null + || payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch) + || current.Attributes["path"].Value != currentDto.Path; + + if (refreshBranch) + { + // remove node if exists + if (current != null) + { + if (current.ParentNode == null) throw new Exception("oops"); + current.ParentNode.RemoveChild(current); + } + + // insert node + var newParent = currentDto.ParentId == -1 + ? safeXml.Xml.DocumentElement + : safeXml.Xml.GetElementById(currentDto.ParentId.ToInvariantString()); + if (newParent == null) continue; + newParent.AppendChild(currentDto.XmlNode); + XmlHelper.SortNode(newParent, ChildNodesXPath, currentDto.XmlNode, + x => x.AttributeValue("sortOrder")); + + // add branch (don't try to be clever) + while (dtos.MoveNext()) + { + // dtos are ordered by sortOrder already + var dto = dtos.Current; + + // if node is already there, somewhere, remove + var n = safeXml.Xml.GetElementById(dto.Id.ToInvariantString()); + if (n != null) + { + if (n.ParentNode == null) throw new Exception("oops"); + n.ParentNode.RemoveChild(n); + } + + // find parent, add node + var p = safeXml.Xml.GetElementById(dto.ParentId.ToInvariantString()); // branch, so parentId > 0 + // takes care of out-of-sync & masked + p?.AppendChild(dto.XmlNode); + } + } + else + { + // in-place + safeXml.Xml = AddOrUpdateXmlNode(safeXml.Xml, currentDto); + } + } + + publishedChanged = true; + } + + if (publishedChanged) + { + safeXml.Commit(); // not auto! + Facade.ResyncCurrent(); + } + } + } + + public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) + { + // see ContentTypeServiceBase + // in all cases we just want to clear the content type cache + // the type will be reloaded if/when needed + foreach (var payload in payloads) + _contentTypeCache.ClearContentType(payload.Id); + + // process content types / content cache + // only those that have been changed - with impact on content - RefreshMain + // for those that have been removed, content is removed already + var ids = payloads + .Where(x => x.ItemType == typeof(IContentType).Name && x.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) + .Select(x => x.Id) + .ToArray(); + + foreach (var payload in payloads) + LogHelper.Debug($"Notified {payload.ChangeTypes} for content type {payload.Id}."); + + if (ids.Length > 0) // must have refreshes, not only removes + RefreshContentTypes(ids); + + // ignore media and member types - we're not caching them + + Facade.ResyncCurrent(); + } + + public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) + { + // see above + // in all cases we just want to clear the content type cache + // the types will be reloaded if/when needed + foreach (var payload in payloads) + _contentTypeCache.ClearDataType(payload.Id); + + foreach (var payload in payloads) + LogHelper.Debug($"Notified {(payload.Removed ? "Removed" : "Refreshed")} for data type {payload.Id}."); + + // that's all we need to do as the changes have NO impact whatsoever on the Xml content + + // ignore media and member types - we're not caching them + + Facade.ResyncCurrent(); + } + + #endregion + + #region Manage change + + private void RefreshContentTypes(IEnumerable ids) + { + using (var safeXml = GetSafeXmlWriter()) + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var xmlDtos = uow.Database.Query(ReadCmsContentXmlForContentTypesSql, + new { @nodeObjectType = new Guid(Constants.ObjectTypes.Document), /*@ids =*/ ids }); + + foreach (var xmlDto in xmlDtos) + { + xmlDto.XmlNode = safeXml.Xml.ReadNode(XmlReader.Create(new StringReader(xmlDto.Xml))); + safeXml.Xml = AddOrUpdateXmlNode(safeXml.Xml, xmlDto); + } + + uow.Complete(); + } + } + + private void RefreshMediaTypes(IEnumerable ids) + { + // nothing to do, we have no cache + } + + private void RefreshMemberTypes(IEnumerable ids) + { + // nothing to do, we have no cache + } + + // adds or updates a node (docNode) into a cache (xml) + private static XmlDocument AddOrUpdateXmlNode(XmlDocument xml, XmlDto xmlDto) + { + // sanity checks + var docNode = xmlDto.XmlNode; + if (xmlDto.Id != docNode.AttributeValue("id")) + throw new ArgumentException("Values of id and docNode/@id are different."); + if (xmlDto.ParentId != docNode.AttributeValue("parentID")) + throw new ArgumentException("Values of parentId and docNode/@parentID are different."); + + // find the document in the cache + XmlNode currentNode = xml.GetElementById(xmlDto.Id.ToInvariantString()); + + // if the document is not there already then it's a new document + // we must make sure that its document type exists in the schema + if (currentNode == null) + { + var xml2 = EnsureSchema(docNode.Name, xml); + if (ReferenceEquals(xml, xml2) == false) + docNode = xml2.ImportNode(docNode, true); + xml = xml2; + } + + // find the parent + XmlNode parentNode = xmlDto.Level == 1 + ? xml.DocumentElement + : xml.GetElementById(xmlDto.ParentId.ToInvariantString()); + + // no parent = cannot do anything + if (parentNode == null) + return xml; + + // insert/move the node under the parent + if (currentNode == null) + { + // document not there, new node, append + currentNode = docNode; + parentNode.AppendChild(currentNode); + } + else + { + // document found... we could just copy the currentNode children nodes over under + // docNode, then remove currentNode and insert docNode... the code below tries to + // be clever and faster, though only benchmarking could tell whether it's worth the + // pain... + + // first copy current parent ID - so we can compare with target parent + var moving = currentNode.AttributeValue("parentID") != xmlDto.ParentId; + + if (docNode.Name == currentNode.Name) + { + // name has not changed, safe to just update the current node + // by transfering values eg copying the attributes, and importing the data elements + TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode); + + // if moving, move the node to the new parent + // else it's already under the right parent + // (but maybe the sort order has been updated) + if (moving) + parentNode.AppendChild(currentNode); // remove then append to parentNode + } + else + { + // name has changed, must use docNode (with new name) + // move children nodes from currentNode to docNode (already has properties) + var children = currentNode.SelectNodes(ChildNodesXPath); + if (children == null) throw new Exception("oops"); + foreach (XmlNode child in children) + docNode.AppendChild(child); // remove then append to docNode + + // and put docNode in the right place - if parent has not changed, then + // just replace, else remove currentNode and insert docNode under the right parent + // (but maybe not at the right position due to sort order) + if (moving) + { + if (currentNode.ParentNode == null) throw new Exception("oops"); + currentNode.ParentNode.RemoveChild(currentNode); + parentNode.AppendChild(docNode); + } + else + { + // replacing might screw the sort order + parentNode.ReplaceChild(docNode, currentNode); + } + + currentNode = docNode; + } + } + + var attrs = currentNode.Attributes; + if (attrs == null) throw new Exception("oops."); + + var attr = attrs["rv"] ?? attrs.Append(xml.CreateAttribute("rv")); + attr.Value = xmlDto.Rv.ToString(CultureInfo.InvariantCulture); + + attr = attrs["path"] ?? attrs.Append(xml.CreateAttribute("path")); + attr.Value = xmlDto.Path; + + // if the nodes are not ordered, must sort + // (see U4-509 + has to work with ReplaceChild too) + //XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue("sortOrder")); + + // but... + // if we assume that nodes are always correctly sorted + // then we just need to ensure that currentNode is at the right position. + // should be faster that moving all the nodes around. + XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + return xml; + } + + private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode documentNode, XmlNode publishedNode) + { + // remove all attributes from the published node + if (publishedNode.Attributes == null) throw new Exception("oops"); + publishedNode.Attributes.RemoveAll(); + + // remove all data nodes from the published node + var dataNodes = publishedNode.SelectNodes(DataNodesXPath); + if (dataNodes == null) throw new Exception("oops"); + foreach (XmlNode n in dataNodes) + publishedNode.RemoveChild(n); + + // append all attributes from the document node to the published node + if (documentNode.Attributes == null) throw new Exception("oops"); + foreach (XmlAttribute att in documentNode.Attributes) + ((XmlElement)publishedNode).SetAttribute(att.Name, att.Value); + + // find the first child node, if any + var childNodes = publishedNode.SelectNodes(ChildNodesXPath); + if (childNodes == null) throw new Exception("oops"); + var firstChildNode = childNodes.Count == 0 ? null : childNodes[0]; + + // append all data nodes from the document node to the published node + dataNodes = documentNode.SelectNodes(DataNodesXPath); + if (dataNodes == null) throw new Exception("oops"); + foreach (XmlNode n in dataNodes) + { + if (publishedNode.OwnerDocument == null) throw new Exception("oops"); + var imported = publishedNode.OwnerDocument.ImportNode(n, true); + if (firstChildNode == null) + publishedNode.AppendChild(imported); + else + publishedNode.InsertBefore(imported, firstChildNode); + } + } + + private static XmlNode ImportContent(XmlDocument xml, XmlDto dto) + { + var node = xml.ReadNode(XmlReader.Create(new StringReader(dto.Xml), new XmlReaderSettings + { + IgnoreWhitespace = true + })); + + if (node == null) throw new Exception("oops"); + if (node.Attributes == null) throw new Exception("oops"); + + var attr = xml.CreateAttribute("rv"); + attr.Value = dto.Rv.ToString(CultureInfo.InvariantCulture); + node.Attributes.Append(attr); + + attr = xml.CreateAttribute("path"); + attr.Value = dto.Path; + node.Attributes.Append(attr); + + return node; + } + + #endregion + + #region Handle Repository Events For Database Xml + + // we need them to be "repository" events ie to trigger from within the repository transaction, + // because they need to be consistent with the content that is being refreshed/removed - and that + // should be guaranteed by a DB transaction + // it is not the case at the moment, instead a global lock is used whenever content is modified - well, + // almost: rollback or unpublish do not implement it - nevertheless + + private void OnContentRemovingEntity(ContentRepository sender, ContentRepository.UnitOfWorkEntityEventArgs args) + { + OnRemovedEntity(args.UnitOfWork.Database, args.Entity); + } + + private void OnMediaRemovingEntity(MediaRepository sender, MediaRepository.UnitOfWorkEntityEventArgs args) + { + OnRemovedEntity(args.UnitOfWork.Database, args.Entity); + } + + private void OnMemberRemovingEntity(MemberRepository sender, MemberRepository.UnitOfWorkEntityEventArgs args) + { + OnRemovedEntity(args.UnitOfWork.Database, args.Entity); + } + + private void OnRemovedEntity(UmbracoDatabase db, IContentBase item) + { + var parms = new { id = item.Id }; + db.Execute("DELETE FROM cmsContentXml WHERE nodeId=@id", parms); + db.Execute("DELETE FROM cmsPreviewXml WHERE nodeId=@id", parms); + + // note: could be optimized by using "WHERE nodeId IN (...)" delete clauses + } + + private void OnContentRemovingVersion(ContentRepository sender, ContentRepository.UnitOfWorkVersionEventArgs args) + { + OnRemovedVersion(args.UnitOfWork.Database, args.EntityId, args.VersionId); + } + + private void OnMediaRemovingVersion(MediaRepository sender, MediaRepository.UnitOfWorkVersionEventArgs args) + { + OnRemovedVersion(args.UnitOfWork.Database, args.EntityId, args.VersionId); + } + + private void OnMemberRemovingVersion(MemberRepository sender, MemberRepository.UnitOfWorkVersionEventArgs args) + { + OnRemovedVersion(args.UnitOfWork.Database, args.EntityId, args.VersionId); + } + + private void OnRemovedVersion(UmbracoDatabase db, int entityId, Guid versionId) + { + // we do not version cmsPreviewXml anymore - nothing to do here + } + + private static readonly string[] PropertiesImpactingAllVersions = { "SortOrder", "ParentId", "Level", "Path", "Trashed" }; + + private static bool HasChangesImpactingAllVersions(IContent icontent) + { + var content = (Core.Models.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(ContentRepository sender, ContentRepository.UnitOfWorkEntityEventArgs args) + { + var db = args.UnitOfWork.Database; + var entity = args.Entity; + + var xml = _xmlContentSerializer(entity).ToDataString(); + + // change below to write only one row - not one per version + var dto1 = new PreviewXmlDto + { + NodeId = entity.Id, + Xml = xml + }; + OnRepositoryRefreshed(db, dto1); + + // if unpublishing, remove from table + + if (((Core.Models.Content)entity).PublishedState == PublishedState.Unpublishing) + { + db.Execute("DELETE FROM cmsContentXml WHERE nodeId=@id", new { id = entity.Id }); + return; + } + + // need to update the published xml if we're saving the published version, + // or having an impact on that version - we update the published xml even when masked + + IContent pc = null; + if (entity.Published) + { + // saving the published version = update xml + pc = entity; + } + else + { + // saving the non-published version, but there is a published version + // check whether we have changes that impact the published version (move...) + if (entity.HasPublishedVersion && HasChangesImpactingAllVersions(entity)) + pc = sender.GetByVersion(entity.PublishedVersionGuid); + } + + if (pc == null) + return; + + xml = _xmlContentSerializer(pc).ToDataString(); + var dto2 = new ContentXmlDto { NodeId = entity.Id, Xml = xml }; + OnRepositoryRefreshed(db, dto2); + + } + + private void OnMediaRefreshedEntity(MediaRepository sender, MediaRepository.UnitOfWorkEntityEventArgs args) + { + var db = args.UnitOfWork.Database; + var entity = args.Entity; + + // for whatever reason we delete some xml when the media is trashed + // at least that's what the MediaService implementation did + if (entity.Trashed) + db.Execute("DELETE FROM cmsContentXml WHERE nodeId=@id", new { id = entity.Id }); + + var xml = _xmlMediaSerializer(entity).ToDataString(); + + var dto1 = new ContentXmlDto { NodeId = entity.Id, Xml = xml }; + OnRepositoryRefreshed(db, dto1); + } + + private void OnMemberRefreshedEntity(MemberRepository sender, MemberRepository.UnitOfWorkEntityEventArgs args) + { + var db = args.UnitOfWork.Database; + var entity = args.Entity; + + var xml = _xmlMemberSerializer(entity).ToDataString(); + + var dto1 = new ContentXmlDto { NodeId = entity.Id, Xml = xml }; + OnRepositoryRefreshed(db, dto1); + } + + private void OnRepositoryRefreshed(UmbracoDatabase db, ContentXmlDto dto) + { + // use a custom SQL to update row version on each update + //db.InsertOrUpdate(dto); + + db.InsertOrUpdate(dto, + "SET xml=@xml, rv=rv+1 WHERE nodeId=@id", + new + { + xml = dto.Xml, + id = dto.NodeId + }); + } + + private void OnRepositoryRefreshed(UmbracoDatabase db, PreviewXmlDto dto) + { + // cannot simply update because of PetaPoco handling of the composite key ;-( + // read http://stackoverflow.com/questions/11169144/how-to-modify-petapoco-class-to-work-with-composite-key-comprising-of-non-numeri + // it works in https://github.com/schotime/PetaPoco and then https://github.com/schotime/NPoco but not here + // + // not important anymore as we don't manage version anymore, + // but: + // + // also + // use a custom SQL to update row version on each update + //db.InsertOrUpdate(dto); + + db.InsertOrUpdate(dto, + "SET xml=@xml, rv=rv+1 WHERE nodeId=@id", + new + { + xml = dto.Xml, + id = dto.NodeId, + }); + } + + private void OnDeletedContent(object sender, Content.ContentDeleteEventArgs args) + { + var db = args.Database; + var parms = new { @nodeId = args.Id }; + db.Execute("DELETE FROM cmsPreviewXml WHERE nodeId=@nodeId", parms); + db.Execute("DELETE FROM cmsContentXml WHERE nodeId=@nodeId", parms); + } + + private void OnContentTypeRefreshedEntity(IContentTypeService sender, ContentTypeChange.EventArgs args) + { + // handling a transaction event that does not play well with cache... + //RepositoryBase.SetCacheEnabledForCurrentRequest(false); // fixme wtf!! + + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var contentTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (contentTypeIds.Any()) + RebuildContentAndPreviewXml(contentTypeIds: contentTypeIds); + } + + private void OnMediaTypeRefreshedEntity(IMediaTypeService sender, ContentTypeChange.EventArgs args) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var mediaTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (mediaTypeIds.Any()) + RebuildMediaXml(contentTypeIds: mediaTypeIds); + } + + private void OnMemberTypeRefreshedEntity(IMemberTypeService sender, ContentTypeChange.EventArgs args) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var memberTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (memberTypeIds.Any()) + RebuildMemberXml(contentTypeIds: memberTypeIds); + } + + #endregion + + #region Rebuild Database Xml + + public void RebuildContentAndPreviewXml(int groupSize = 5000, IEnumerable contentTypeIds = null) + { + var contentTypeIdsA = contentTypeIds?.ToArray(); + + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + RebuildContentXmlLocked(uow, repository, groupSize, contentTypeIdsA); + RebuildPreviewXmlLocked(uow, repository, groupSize, contentTypeIdsA); + uow.Complete(); + } + } + + public void RebuildContentXml(int groupSize = 5000, IEnumerable contentTypeIds = null) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + RebuildContentXmlLocked(uow, repository, groupSize, contentTypeIds); + uow.Complete(); + } + } + + // assumes content tree lock + private void RebuildContentXmlLocked(IDatabaseUnitOfWork unitOfWork, IContentRepository repository, int groupSize, IEnumerable contentTypeIds) + { + var contentTypeIdsA = contentTypeIds?.ToArray(); + var contentObjectType = Guid.Parse(Constants.ObjectTypes.Document); + var db = unitOfWork.Database; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIdsA.Length == 0) + { + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //WHERE umbracoNode.nodeObjectType=@objType", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = contentObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //JOIN cmsContent ON (cmsContentXml.nodeId=cmsContent.nodeId) + //WHERE umbracoNode.nodeObjectType=@objType + //AND cmsContent.contentType IN (@ctypes)", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode + JOIN cmsContent ON cmsContent.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND cmsContent.contentType IN (@ctypes) +)", + new { objType = contentObjectType, ctypes = contentTypeIdsA }); + } + + // insert back - if anything fails the transaction will rollback + var query = repository.Query.Where(x => x.Published); + if (contentTypeIds != null && contentTypeIdsA.Length > 0) + query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // make sure we do NOT add (cmsDocument.newest = 1) to the query + // because we already have the condition on the content being published + var descendants = repository.GetPagedResultsByQuery(query, pageIndex++, groupSize, out total, "Path", Direction.Ascending, true, newest: false); + var items = descendants.Select(c => new ContentXmlDto { NodeId = c.Id, Xml = _xmlContentSerializer(c).ToDataString() }).ToArray(); + db.BulkInsertRecords(db.SqlSyntax, items, null, false); // run within the current transaction and do NOT commit + processed += items.Length; + } while (processed < total); + } + + public void RebuildPreviewXml(int groupSize = 5000, IEnumerable contentTypeIds = null) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + RebuildPreviewXmlLocked(uow, repository, groupSize, contentTypeIds); + uow.Complete(); + } + } + + // assumes content tree lock + private void RebuildPreviewXmlLocked(IDatabaseUnitOfWork unitOfWork, IContentRepository repository, int groupSize, IEnumerable contentTypeIds) + { + var contentTypeIdsA = contentTypeIds?.ToArray(); + var contentObjectType = Guid.Parse(Constants.ObjectTypes.Document); + var db = unitOfWork.Database; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIdsA.Length == 0) + { + // must support SQL-CE + // db.Execute(@"DELETE cmsPreviewXml + //FROM cmsPreviewXml + //JOIN umbracoNode ON (cmsPreviewXml.nodeId=umbracoNode.Id) + //WHERE umbracoNode.nodeObjectType=@objType", + db.Execute(@"DELETE FROM cmsPreviewXml +WHERE cmsPreviewXml.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = contentObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + // db.Execute(@"DELETE cmsPreviewXml + //FROM cmsPreviewXml + //JOIN umbracoNode ON (cmsPreviewXml.nodeId=umbracoNode.Id) + //JOIN cmsContent ON (cmsPreviewXml.nodeId=cmsContent.nodeId) + //WHERE umbracoNode.nodeObjectType=@objType + //AND cmsContent.contentType IN (@ctypes)", + db.Execute(@"DELETE FROM cmsPreviewXml +WHERE cmsPreviewXml.nodeId IN ( + SELECT id FROM umbracoNode + JOIN cmsContent ON cmsContent.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND cmsContent.contentType IN (@ctypes) +)", + new { objType = contentObjectType, ctypes = contentTypeIdsA }); + } + + // insert back - if anything fails the transaction will rollback + var query = repository.Query; + if (contentTypeIds != null && contentTypeIdsA.Length > 0) + query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // .GetPagedResultsByQuery implicitely adds (cmsDocument.newest = 1) which + // is what we want for preview (ie latest version of a content, published or not) + var descendants = repository.GetPagedResultsByQuery(query, pageIndex++, groupSize, out total, "Path", Direction.Ascending, true); + var items = descendants.Select(c => new PreviewXmlDto + { + NodeId = c.Id, + Xml = _xmlContentSerializer(c).ToDataString() + }).ToArray(); + db.BulkInsertRecords(db.SqlSyntax, items, null, false); // run within the current transaction and do NOT commit + processed += items.Length; + } while (processed < total); + } + + public void RebuildMediaXml(int groupSize = 5000, IEnumerable contentTypeIds = null) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.MediaTree); + var repository = uow.CreateRepository(); + RebuildMediaXmlLocked(uow, repository, groupSize, contentTypeIds); + uow.Complete(); + } + } + + // assumes media tree lock + public void RebuildMediaXmlLocked(IDatabaseUnitOfWork unitOfWork, IMediaRepository repository, int groupSize, IEnumerable contentTypeIds) + { + var contentTypeIdsA = contentTypeIds?.ToArray(); + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var db = unitOfWork.Database; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIdsA.Length == 0) + { + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //WHERE umbracoNode.nodeObjectType=@objType", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = mediaObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //JOIN cmsContent ON (cmsContentXml.nodeId=cmsContent.nodeId) + //WHERE umbracoNode.nodeObjectType=@objType + //AND cmsContent.contentType IN (@ctypes)", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode + JOIN cmsContent ON cmsContent.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND cmsContent.contentType IN (@ctypes) +)", + new { objType = mediaObjectType, ctypes = contentTypeIdsA }); + } + + // insert back - if anything fails the transaction will rollback + var query = repository.Query; + if (contentTypeIds != null && contentTypeIdsA.Length > 0) + query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) + + long pageIndex = 0; + long processed = 0; + long total; + do + { + var descendants = repository.GetPagedResultsByQuery(query, pageIndex++, groupSize, out total, "Path", Direction.Ascending, true); + var items = descendants.Select(m => new ContentXmlDto { NodeId = m.Id, Xml = _xmlMediaSerializer(m).ToDataString() }).ToArray(); + db.BulkInsertRecords(db.SqlSyntax, items, null, false); // run within the current transaction and do NOT commit + processed += items.Length; + } while (processed < total); + } + + public void RebuildMemberXml(int groupSize = 5000, IEnumerable contentTypeIds = null) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.MemberTree); + var repository = uow.CreateRepository(); + RebuildMemberXmlLocked(uow, repository, groupSize, contentTypeIds); + uow.Complete(); + } + } + + // assumes member tree lock + public void RebuildMemberXmlLocked(IDatabaseUnitOfWork unitOfWork, IMemberRepository repository, int groupSize, IEnumerable contentTypeIds) + { + var contentTypeIdsA = contentTypeIds?.ToArray(); + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var db = unitOfWork.Database; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIdsA.Length == 0) + { + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //WHERE umbracoNode.nodeObjectType=@objType", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = memberObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + // db.Execute(@"DELETE cmsContentXml + //FROM cmsContentXml + //JOIN umbracoNode ON (cmsContentXml.nodeId=umbracoNode.Id) + //JOIN cmsContent ON (cmsContentXml.nodeId=cmsContent.nodeId) + //WHERE umbracoNode.nodeObjectType=@objType + //AND cmsContent.contentType IN (@ctypes)", + db.Execute(@"DELETE FROM cmsContentXml +WHERE cmsContentXml.nodeId IN ( + SELECT id FROM umbracoNode + JOIN cmsContent ON cmsContent.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND cmsContent.contentType IN (@ctypes) +)", + new { objType = memberObjectType, ctypes = contentTypeIdsA }); + } + + // insert back - if anything fails the transaction will rollback + var query = repository.Query; + if (contentTypeIds != null && contentTypeIdsA.Length > 0) + query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) + + long pageIndex = 0; + long processed = 0; + long total; + do + { + var descendants = repository.GetPagedResultsByQuery(query, pageIndex++, groupSize, out total, "Path", Direction.Ascending, true); + var items = descendants.Select(m => new ContentXmlDto { NodeId = m.Id, Xml = _xmlMemberSerializer(m).ToDataString() }).ToArray(); + db.BulkInsertRecords(db.SqlSyntax, items, null, false); // run within the current transaction and do NOT commit + processed += items.Length; + } while (processed < total); + } + + public bool VerifyContentAndPreviewXml() + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var ok = VerifyContentAndPreviewXmlLocked(uow, repository); + uow.Complete(); + return ok; + } + } + + // assumes content tree lock + private bool VerifyContentAndPreviewXmlLocked(IDatabaseUnitOfWork unitOfWork, IContentRepository repository) + { + // every published content item should have a corresponding row in cmsContentXml + // every content item should have a corresponding row in cmsPreviewXml + + var contentObjectType = Guid.Parse(Constants.ObjectTypes.Document); + var db = unitOfWork.Database; + + var count = db.ExecuteScalar(@"SELECT COUNT(*) +FROM umbracoNode +JOIN cmsDocument ON (umbracoNode.id=cmsDocument.nodeId and cmsDocument.published=1) +LEFT JOIN cmsContentXml ON (umbracoNode.id=cmsContentXml.nodeId) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentXml.nodeId IS NULL +", new { objType = contentObjectType }); + + if (count > 0) return false; + + count = db.ExecuteScalar(@"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsPreviewXml ON (umbracoNode.id=cmsPreviewXml.nodeId) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsPreviewXml.nodeId IS NULL +", new { objType = contentObjectType }); + + return count == 0; + } + + public bool VerifyMediaXml() + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.MediaTree); + var repository = uow.CreateRepository(); + var ok = VerifyMediaXmlLocked(uow, repository); + uow.Complete(); + return ok; + } + } + + // assumes media tree lock + public bool VerifyMediaXmlLocked(IDatabaseUnitOfWork unitOfWork, IMediaRepository repository) + { + // every non-trashed media item should have a corresponding row in cmsContentXml + + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var db = unitOfWork.Database; + + var count = db.ExecuteScalar(@"SELECT COUNT(*) +FROM umbracoNode +JOIN cmsDocument ON (umbracoNode.id=cmsDocument.nodeId and cmsDocument.published=1) +LEFT JOIN cmsContentXml ON (umbracoNode.id=cmsContentXml.nodeId) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentXml.nodeId IS NULL +", new { objType = mediaObjectType }); + + return count == 0; + } + + public bool VerifyMemberXml() + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.MemberTree); + var repository = uow.CreateRepository(); + var ok = VerifyMemberXmlLocked(uow, repository); + uow.Complete(); + return ok; + } + } + + // assumes member tree lock + public bool VerifyMemberXmlLocked(IDatabaseUnitOfWork unitOfWork, IMemberRepository repository) + { + // every member item should have a corresponding row in cmsContentXml + + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var db = unitOfWork.Database; + + var count = db.ExecuteScalar(@"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsContentXml ON (umbracoNode.id=cmsContentXml.nodeId) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentXml.nodeId IS NULL +", new { objType = memberObjectType }); + + return count == 0; + } + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStoreFilePersister.cs similarity index 71% rename from src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs rename to src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStoreFilePersister.cs index 3fa11a0dc2..a9c1c1bd20 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStoreFilePersister.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; -using umbraco; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Scheduling; @@ -17,11 +16,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : LatchedBackgroundTaskBase + internal class XmlStoreFilePersister : LatchedBackgroundTaskBase // FIXME compare to the one we have already { - private readonly IBackgroundTaskRunner _runner; - private readonly content _content; - private readonly ProfilingLogger _logger; + private readonly IBackgroundTaskRunner _runner; + private readonly ILogger _logger; + private readonly XmlStore _store; private readonly object _locko = new object(); private bool _released; private Timer _timer; @@ -38,20 +37,21 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down - public override bool RunsOnShutdown { get { return _timer != null; } } + public override bool RunsOnShutdown => _timer != null; // initialize the first instance, which is inactive (not touched yet) - public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, ProfilingLogger logger) - : this(runner, content, logger, false) + public XmlStoreFilePersister(IBackgroundTaskRunner runner, XmlStore store, ILogger logger) + : this(runner, store, logger, false) { } - private XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, ProfilingLogger logger, bool touched) + // initialize further instances, which are active (touched) + private XmlStoreFilePersister(IBackgroundTaskRunner runner, XmlStore store, ILogger logger, bool touched) { _runner = runner; - _content = content; + _store = store; _logger = logger; - if (runner.TryAdd(this) == false) + if (_runner.TryAdd(this) == false) { _runner = null; // runner's down _released = true; // don't mess with timer @@ -62,13 +62,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (touched == false) return; - _logger.Logger.Debug("Created, save in {0}ms.", () => WaitMilliseconds); + _logger.Debug("Created, save in {0}ms.", () => WaitMilliseconds); _initialTouch = DateTime.Now; _timer = new Timer(_ => TimerRelease()); _timer.Change(WaitMilliseconds, 0); } - public XmlCacheFilePersister Touch() + public XmlStoreFilePersister Touch() { // if _released is false then we're going to setup a timer // then the runner wants to shutdown & run immediately @@ -84,22 +84,22 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (_released) // our timer has triggered OR the runner is shutting down { - _logger.Logger.Debug("Touched, was released..."); + _logger.Debug("Touched, was released..."); // release: has run or is running, too late, return a new task (adds itself to runner) if (_runner == null) { - _logger.Logger.Debug("Runner is down, run now."); + _logger.Debug("Runner is down, run now."); runNow = true; } else { - _logger.Logger.Debug("Create new..."); - ret = new XmlCacheFilePersister(_runner, _content, _logger, true); + _logger.Debug("Create new..."); + ret = new XmlStoreFilePersister(_runner, _store, _logger, true); if (ret._runner == null) { // could not enlist with the runner, runner is completed, must run now - _logger.Logger.Debug("Runner is down, run now."); + _logger.Debug("Runner is down, run now."); runNow = true; } } @@ -107,7 +107,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache else if (_timer == null) // we don't have a timer yet { - _logger.Logger.Debug("Touched, was idle, start and save in {0}ms.", () => WaitMilliseconds); + _logger.Debug("Touched, was idle, start and save in {0}ms.", () => WaitMilliseconds); _initialTouch = DateTime.Now; _timer = new Timer(_ => TimerRelease()); _timer.Change(WaitMilliseconds, 0); @@ -120,19 +120,23 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds)) { - _logger.Logger.Debug("Touched, was waiting, can delay, save in {0}ms.", () => WaitMilliseconds); + _logger.Debug("Touched, was waiting, can delay, save in {0}ms.", () => WaitMilliseconds); _timer.Change(WaitMilliseconds, 0); } else { - _logger.Logger.Debug("Touched, was waiting, cannot delay."); + _logger.Debug("Touched, was waiting, cannot delay."); } } } + // note: this comes from 7.x where it was not possible to lock the entire content service + // in our case, the XmlStore configures everything so that it is not possible to access content + // when going down, so this should never happen. + if (runNow) //Run(); - LogHelper.Warn("Cannot write now because we are going down, changes may be lost."); + _logger.Warn("Cannot write now because we are going down, changes may be lost."); return ret; // this, by default, unless we created a new one } @@ -141,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { lock (_locko) { - _logger.Logger.Debug("Timer: release."); + _logger.Debug("Timer: release."); _released = true; Release(); @@ -152,7 +156,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { lock (_locko) { - _logger.Logger.Debug("Run now (async)."); + _logger.Debug("Run now (async)."); // just make sure - in case the runner is running the task on shutdown _released = true; } @@ -160,7 +164,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // http://stackoverflow.com/questions/13489065/best-practice-to-call-configureawait-for-all-server-side-code // http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html // do we really need that ConfigureAwait here? - + // - In theory, no, because we are already executing on a background thread because we know it is there and // there won't be any SynchronizationContext to resume to, however this is 'library' code and // who are we to say that this will never be executed in a sync context... this is best practice to be sure @@ -169,27 +173,24 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache using (await _runLock.LockAsync()) { - await _content.SaveXmlToFileAsync().ConfigureAwait(false); + await _store.SaveXmlToFileAsync().ConfigureAwait(false); } } - public override bool IsAsync - { - get { return true; } - } + public override bool IsAsync => true; public override void Run() { lock (_locko) { - _logger.Logger.Debug("Run now (sync)."); + _logger.Debug("Run now (sync)."); // not really needed but safer (it's only us invoking Run, but the method is public...) _released = true; } using (_runLock.Lock()) { - _content.SaveXmlToFile(); + _store.SaveXmlToFile(); } } diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 97006bbeae..e3431a5e80 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -19,18 +19,18 @@ namespace Umbraco.Web { private readonly ITypedPublishedContentQuery _typedContentQuery; private readonly IDynamicPublishedContentQuery _dynamicContentQuery; - private readonly ContextualPublishedContentCache _contentCache; - private readonly ContextualPublishedMediaCache _mediaCache; + private readonly IPublishedContentCache _contentCache; + private readonly IPublishedMediaCache _mediaCache; /// /// Constructor used to return results from the caches /// /// /// - public PublishedContentQuery(ContextualPublishedContentCache contentCache, ContextualPublishedMediaCache mediaCache) + public PublishedContentQuery(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) { - if (contentCache == null) throw new ArgumentNullException("contentCache"); - if (mediaCache == null) throw new ArgumentNullException("mediaCache"); + if (contentCache == null) throw new ArgumentNullException(nameof(contentCache)); + if (mediaCache == null) throw new ArgumentNullException(nameof(mediaCache)); _contentCache = contentCache; _mediaCache = mediaCache; } @@ -42,8 +42,8 @@ namespace Umbraco.Web /// public PublishedContentQuery(ITypedPublishedContentQuery typedContentQuery, IDynamicPublishedContentQuery dynamicContentQuery) { - if (typedContentQuery == null) throw new ArgumentNullException("typedContentQuery"); - if (dynamicContentQuery == null) throw new ArgumentNullException("dynamicContentQuery"); + if (typedContentQuery == null) throw new ArgumentNullException(nameof(typedContentQuery)); + if (dynamicContentQuery == null) throw new ArgumentNullException(nameof(dynamicContentQuery)); _typedContentQuery = typedContentQuery; _dynamicContentQuery = dynamicContentQuery; } @@ -191,48 +191,48 @@ namespace Umbraco.Web #region Used by Content/Media - private IPublishedContent TypedDocumentById(int id, ContextualPublishedCache cache) + private IPublishedContent TypedDocumentById(int id, IPublishedCache cache) { var doc = cache.GetById(id); return doc; } - private IPublishedContent TypedDocumentByXPath(string xpath, XPathVariable[] vars, ContextualPublishedContentCache cache) + private IPublishedContent TypedDocumentByXPath(string xpath, XPathVariable[] vars, IPublishedContentCache cache) { var doc = cache.GetSingleByXPath(xpath, vars); return doc; } //NOTE: Not used? - //private IPublishedContent TypedDocumentByXPath(XPathExpression xpath, XPathVariable[] vars, ContextualPublishedContentCache cache) + //private IPublishedContent TypedDocumentByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedContentCache cache) //{ // var doc = cache.GetSingleByXPath(xpath, vars); // return doc; //} - private IEnumerable TypedDocumentsByIds(ContextualPublishedCache cache, IEnumerable ids) + private IEnumerable TypedDocumentsByIds(IPublishedCache cache, IEnumerable ids) { return ids.Select(eachId => TypedDocumentById(eachId, cache)).WhereNotNull(); } - private IEnumerable TypedDocumentsByXPath(string xpath, XPathVariable[] vars, ContextualPublishedContentCache cache) + private IEnumerable TypedDocumentsByXPath(string xpath, XPathVariable[] vars, IPublishedContentCache cache) { var doc = cache.GetByXPath(xpath, vars); return doc; } - private IEnumerable TypedDocumentsByXPath(XPathExpression xpath, XPathVariable[] vars, ContextualPublishedContentCache cache) + private IEnumerable TypedDocumentsByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedContentCache cache) { var doc = cache.GetByXPath(xpath, vars); return doc; } - private IEnumerable TypedDocumentsAtRoot(ContextualPublishedCache cache) + private IEnumerable TypedDocumentsAtRoot(IPublishedCache cache) { return cache.GetAtRoot(); } - private dynamic DocumentById(int id, ContextualPublishedCache cache, object ifNotFound) + private dynamic DocumentById(int id, IPublishedCache cache, object ifNotFound) { var doc = cache.GetById(id); return doc == null @@ -240,7 +240,7 @@ namespace Umbraco.Web : new DynamicPublishedContent(doc).AsDynamic(); } - private dynamic DocumentByXPath(string xpath, XPathVariable[] vars, ContextualPublishedCache cache, object ifNotFound) + private dynamic DocumentByXPath(string xpath, XPathVariable[] vars, IPublishedCache cache, object ifNotFound) { var doc = cache.GetSingleByXPath(xpath, vars); return doc == null @@ -248,7 +248,7 @@ namespace Umbraco.Web : new DynamicPublishedContent(doc).AsDynamic(); } - private dynamic DocumentByXPath(XPathExpression xpath, XPathVariable[] vars, ContextualPublishedCache cache, object ifNotFound) + private dynamic DocumentByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedCache cache, object ifNotFound) { var doc = cache.GetSingleByXPath(xpath, vars); return doc == null @@ -256,7 +256,7 @@ namespace Umbraco.Web : new DynamicPublishedContent(doc).AsDynamic(); } - private dynamic DocumentByIds(ContextualPublishedCache cache, IEnumerable ids) + private dynamic DocumentByIds(IPublishedCache cache, IEnumerable ids) { var dNull = DynamicNull.Null; var nodes = ids.Select(eachId => DocumentById(eachId, cache, dNull)) @@ -265,7 +265,7 @@ namespace Umbraco.Web return new DynamicPublishedContentList(nodes); } - private dynamic DocumentsByXPath(string xpath, XPathVariable[] vars, ContextualPublishedCache cache) + private dynamic DocumentsByXPath(string xpath, XPathVariable[] vars, IPublishedCache cache) { return new DynamicPublishedContentList( cache.GetByXPath(xpath, vars) @@ -273,7 +273,7 @@ namespace Umbraco.Web ); } - private dynamic DocumentsByXPath(XPathExpression xpath, XPathVariable[] vars, ContextualPublishedCache cache) + private dynamic DocumentsByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedCache cache) { return new DynamicPublishedContentList( cache.GetByXPath(xpath, vars) @@ -281,7 +281,7 @@ namespace Umbraco.Web ); } - private dynamic DocumentsAtRoot(ContextualPublishedCache cache) + private dynamic DocumentsAtRoot(IPublishedCache cache) { return new DynamicPublishedContentList( cache.GetAtRoot() diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs index 0ce9cd28dd..d78d9e6046 100644 --- a/src/Umbraco.Web/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -62,7 +62,7 @@ namespace Umbraco.Web.Routing if (string.IsNullOrWhiteSpace(umbracoUrlName)) return Enumerable.Empty(); - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); + var domainHelper = new DomainHelper(umbracoContext.Facade.DomainCache); var n = node; var domainUris = domainHelper.DomainsForNode(n.Id, current, false); diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs index b3377a0078..c285aa65c7 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Routing /// public class ContentFinderByNiceUrl : IContentFinder { - protected ILogger Logger { get; private set; } + protected ILogger Logger { get; } public ContentFinderByNiceUrl(ILogger logger) { @@ -28,7 +28,7 @@ namespace Umbraco.Web.Routing { string route; if (docRequest.HasDomain) - route = docRequest.Domain.RootNodeId + DomainHelper.PathRelativeToDomain(docRequest.DomainUri, docRequest.Uri.GetAbsolutePathDecoded()); + route = docRequest.Domain.ContentId + DomainHelper.PathRelativeToDomain(docRequest.Domain.Uri, docRequest.Uri.GetAbsolutePathDecoded()); else route = docRequest.Uri.GetAbsolutePathDecoded(); diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs index ad66f9705b..26fdb7a76d 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs @@ -16,22 +16,21 @@ namespace Umbraco.Web.Routing { public ContentFinderByNiceUrlAndTemplate(ILogger logger) : base(logger) - { - } + { } /// /// Tries to find and assign an Umbraco document to a PublishedContentRequest. /// - /// The PublishedContentRequest. + /// The PublishedContentRequest. /// A value indicating whether an Umbraco document was found and assigned. /// If successful, also assigns the template. public override bool TryFindContent(PublishedContentRequest docRequest) { IPublishedContent node = null; - string path = docRequest.Uri.GetAbsolutePathDecoded(); + var path = docRequest.Uri.GetAbsolutePathDecoded(); if (docRequest.HasDomain) - path = DomainHelper.PathRelativeToDomain(docRequest.DomainUri, path); + path = DomainHelper.PathRelativeToDomain(docRequest.Domain.Uri, path); if (path != "/") // no template if "/" { @@ -44,7 +43,7 @@ namespace Umbraco.Web.Routing { Logger.Debug("Valid template: \"{0}\"", () => templateAlias); - var route = docRequest.HasDomain ? (docRequest.Domain.RootNodeId.ToString() + path) : path; + var route = docRequest.HasDomain ? (docRequest.Domain.ContentId.ToString() + path) : path; node = FindContent(docRequest, route); if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false && node != null) diff --git a/src/Umbraco.Web/Routing/ContentFinderByProfile.cs b/src/Umbraco.Web/Routing/ContentFinderByProfile.cs index d2fb3c2ce8..c495323a0d 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByProfile.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByProfile.cs @@ -17,20 +17,19 @@ namespace Umbraco.Web.Routing { public ContentFinderByProfile(ILogger logger) : base(logger) - { - } + { } /// /// Tries to find and assign an Umbraco document to a PublishedContentRequest. /// - /// The PublishedContentRequest. + /// The PublishedContentRequest. /// A value indicating whether an Umbraco document was found and assigned. public override bool TryFindContent(PublishedContentRequest docRequest) { IPublishedContent node = null; var path = docRequest.Uri.GetAbsolutePathDecoded(); - bool isProfile = false; + var isProfile = false; var pos = path.LastIndexOf('/'); if (pos > 0) { @@ -42,7 +41,7 @@ namespace Umbraco.Web.Routing isProfile = true; Logger.Debug("Path \"{0}\" is the profile path", () => path); - var route = docRequest.HasDomain ? (docRequest.Domain.RootNodeId.ToString() + path) : path; + var route = docRequest.HasDomain ? (docRequest.Domain.ContentId + path) : path; node = FindContent(docRequest, route); if (node != null) @@ -57,7 +56,7 @@ namespace Umbraco.Web.Routing } } - if (!isProfile) + if (isProfile == false) { Logger.Debug("Not the profile path"); } diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index f4c73e4b5d..5070b831cd 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -1,7 +1,6 @@ using System; using System.Text; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core; @@ -19,7 +18,7 @@ namespace Umbraco.Web.Routing /// public class ContentFinderByUrlAlias : IContentFinder { - protected ILogger Logger { get; private set; } + protected ILogger Logger { get; } public ContentFinderByUrlAlias(ILogger logger) { @@ -38,7 +37,7 @@ namespace Umbraco.Web.Routing if (docRequest.Uri.AbsolutePath != "/") // no alias if "/" { node = FindContentByAlias(docRequest.RoutingContext.UmbracoContext.ContentCache, - docRequest.HasDomain ? docRequest.Domain.RootNodeId : 0, + docRequest.HasDomain ? docRequest.Domain.ContentId : 0, docRequest.Uri.GetAbsolutePathDecoded()); if (node != null) @@ -51,9 +50,9 @@ namespace Umbraco.Web.Routing return node != null; } - private static IPublishedContent FindContentByAlias(ContextualPublishedContentCache cache, int rootNodeId, string alias) + private static IPublishedContent FindContentByAlias(IPublishedContentCache cache, int rootNodeId, string alias) { - if (alias == null) throw new ArgumentNullException("alias"); + if (alias == null) throw new ArgumentNullException(nameof(alias)); // the alias may be "foo/bar" or "/foo/bar" // there may be spaces as in "/foo/bar, /foo/nil" @@ -61,7 +60,7 @@ namespace Umbraco.Web.Routing alias = alias.TrimStart('/'); var xpathBuilder = new StringBuilder(); - xpathBuilder.Append(XPathStringsDefinition.Root); + xpathBuilder.Append(XPathStrings.Root); if (rootNodeId > 0) xpathBuilder.AppendFormat(XPathStrings.DescendantDocumentById, rootNodeId); @@ -83,59 +82,14 @@ namespace Umbraco.Web.Routing #region XPath Strings - class XPathStringsDefinition - { - public int Version { get; private set; } - - public static string Root { get { return "/root"; } } - public string DescendantDocumentById { get; private set; } - public string DescendantDocumentByAlias { get; private set; } - - public XPathStringsDefinition(int version) - { - Version = version; - - switch (version) - { - // legacy XML schema - case 0: - DescendantDocumentById = "//node [@id={0}]"; - DescendantDocumentByAlias = "//node[(" - + "contains(concat(',',translate(data [@alias='umbracoUrlAlias'], ' ', ''),','),',{0},')" - + " or contains(concat(',',translate(data [@alias='umbracoUrlAlias'], ' ', ''),','),',/{0},')" - + ")]"; - break; - - // default XML schema as of 4.10 - case 1: - DescendantDocumentById = "//* [@isDoc and @id={0}]"; - DescendantDocumentByAlias = "//* [@isDoc and (" - + "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" - + " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" - + ")]"; - break; - - default: - throw new Exception(string.Format("Unsupported Xml schema version '{0}').", version)); - } - } - } - - static XPathStringsDefinition _xPathStringsValue; - static XPathStringsDefinition XPathStrings - { - get - { - // in theory XPathStrings should be a static variable that - // we should initialize in a static ctor - but then test cases - // that switch schemas fail - so cache and refresh when needed, - // ie never when running the actual site - - var version = 1; - if (_xPathStringsValue == null || _xPathStringsValue.Version != version) - _xPathStringsValue = new XPathStringsDefinition(version); - return _xPathStringsValue; - } + static class XPathStrings + { + public static string Root => "/root"; + public const string DescendantDocumentById = "//* [@isDoc and @id={0}]"; + public const string DescendantDocumentByAlias = "//* [@isDoc and (" + + "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + + " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + + ")]"; } #endregion diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 221f74eb93..ea1b96c091 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.Routing return null; } - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); + var domainHelper = new DomainHelper(umbracoContext.Facade.DomainCache); // extract domainUri and path // route is / or / @@ -96,7 +96,7 @@ namespace Umbraco.Web.Routing return null; } - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); + var domainHelper = new DomainHelper(umbracoContext.Facade.DomainCache); // extract domainUri and path // route is / or / diff --git a/src/Umbraco.Web/Routing/Domain.cs b/src/Umbraco.Web/Routing/Domain.cs new file mode 100644 index 0000000000..f74422d25d --- /dev/null +++ b/src/Umbraco.Web/Routing/Domain.cs @@ -0,0 +1,65 @@ +using System.Globalization; + +namespace Umbraco.Web.Routing +{ + /// + /// Represents a facade domain. + /// + public class Domain + { + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the domain. + /// The name of the domain. + /// The identifier of the content which supports the domain. + /// The culture of the domain. + /// A value indicating whether the domain is a wildcard domain. + public Domain(int id, string name, int contentId, CultureInfo culture, bool isWildcard) + { + Id = id; + Name = name; + ContentId = contentId; + Culture = culture; + IsWildcard = isWildcard; + } + + /// + /// Initializes a new instance of the class. + /// + /// An origin domain. + protected Domain(Domain domain) + { + Id = domain.Id; + Name = domain.Name; + ContentId = domain.ContentId; + Culture = domain.Culture; + IsWildcard = domain.IsWildcard; + } + + /// + /// Gets the unique identifier of the domain. + /// + public int Id { get; } + + /// + /// Gets the name of the domain. + /// + public string Name { get; } + + /// + /// Gets the identifier of the content which supports the domain. + /// + public int ContentId { get; } + + /// + /// Gets the culture of the domain. + /// + public CultureInfo Culture { get; } + + /// + /// Gets a value indicating whether the domain is a wildcard domain. + /// + public bool IsWildcard { get; } + } +} diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs index 6395e937df..10958fe1a2 100644 --- a/src/Umbraco.Web/Routing/DomainAndUri.cs +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -1,8 +1,5 @@ using System; -using System.ComponentModel; using Umbraco.Core; -using umbraco.cms.businesslogic.web; -using Umbraco.Core.Models; namespace Umbraco.Web.Routing { @@ -13,54 +10,37 @@ namespace Umbraco.Web.Routing /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. /// - public class DomainAndUri + public class DomainAndUri : Domain { /// /// Initializes a new instance of the class with a Domain and a uri scheme. /// - /// The domain. - /// The uri scheme. - public DomainAndUri(IDomain domain, string scheme) + /// The original domain. + /// The context current Uri. + public DomainAndUri(Domain domain, Uri currentUri) + : base(domain) { - UmbracoDomain = domain; try { - Uri = new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(domain.DomainName, scheme))); + // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri + var name = Name.StartsWith("/") && currentUri != null + ? currentUri.GetLeftPart(UriPartial.Authority) + Name + : Name; + var scheme = currentUri?.Scheme ?? Uri.UriSchemeHttp; + Uri = new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(name, scheme))); } catch (UriFormatException) { - var name = domain.DomainName.ToCSharpString(); - throw new ArgumentException(string.Format("Failed to parse invalid domain: node id={0}, hostname=\"{1}\"." - + " Hostname should be a valid uri.", domain.RootContentId, name), "domain"); + throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." + + " Hostname should be a valid uri.", nameof(domain)); } } - [Obsolete("This should not be used, use the other contructor specifying the non legacy IDomain instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public DomainAndUri(Domain domain, string scheme) - : this(domain.DomainEntity, scheme) - { - - } - - - [Obsolete("This should not be used, use the non-legacy property called UmbracoDomain instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public Domain Domain - { - get { return new Domain(UmbracoDomain); } - } - - /// - /// Gets the Umbraco domain. - /// - public IDomain UmbracoDomain { get; private set; } - /// /// Gets or sets the normalized uri of the domain. /// - public Uri Uri { get; private set; } + public Uri Uri { get; } /// /// Gets a string that represents the instance. @@ -68,7 +48,7 @@ namespace Umbraco.Web.Routing /// A string that represents the current instance. public override string ToString() { - return string.Format("{{ \"{0}\", \"{1}\" }}", UmbracoDomain.DomainName, Uri); + return $"{{ \"{Name}\", \"{Uri}\" }}"; } } } diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 6934b08cf2..571c2db6c9 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; +using Umbraco.Web.PublishedCache; // Facade namespace Umbraco.Web.Routing { @@ -12,17 +11,11 @@ namespace Umbraco.Web.Routing /// public class DomainHelper { - private readonly IDomainService _domainService; + private readonly IDomainCache _domainCache; - [Obsolete("Use the contructor specifying all dependencies instead")] - public DomainHelper() - : this(ApplicationContext.Current.Services.DomainService) + public DomainHelper(IDomainCache domainCache) { - } - - public DomainHelper(IDomainService domainService) - { - _domainService = domainService; + _domainCache = domainCache; } #region Domain for Node @@ -42,10 +35,10 @@ namespace Umbraco.Web.Routing return null; // get the domains on that node - var domains = _domainService.GetAssignedDomains(nodeId, false).ToArray(); + var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); // none? - if (domains.Any() == false) + if (domains.Length == 0) return null; // else filter @@ -65,7 +58,7 @@ namespace Umbraco.Web.Routing /// True if the node has domains, else false. internal bool NodeHasDomains(int nodeId) { - return nodeId > 0 && _domainService.GetAssignedDomains(nodeId, false).Any(); + return nodeId > 0 && _domainCache.GetAssigned(nodeId, false).Any(); } /// @@ -84,10 +77,10 @@ namespace Umbraco.Web.Routing return null; // get the domains on that node - var domains = _domainService.GetAssignedDomains(nodeId, false).ToArray(); + var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); // none? - if (domains.Any() == false) + if (domains.Length == 0) return null; // get the domains and their uris @@ -114,20 +107,19 @@ namespace Umbraco.Web.Routing /// the right one, unless it is null, in which case the method returns null. /// The filter, if any, will be called only with a non-empty argument, and _must_ return something. /// - internal static DomainAndUri DomainForUri(IEnumerable domains, Uri current, Func filter = null) + internal static DomainAndUri DomainForUri(IEnumerable domains, Uri current, Func filter = null) { // sanitize the list to have proper uris for comparison (scheme, path end with /) // we need to end with / because example.com/foo cannot match example.com/foobar - // we need to order so example.com/foo matches before example.com/ - var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; + // we need to order so example.com/foo matches before example.com/ var domainsAndUris = domains .Where(d => d.IsWildcard == false) - .Select(SanitizeForBackwardCompatibility) - .Select(d => new DomainAndUri(d, scheme)) + //.Select(SanitizeForBackwardCompatibility) + .Select(d => new DomainAndUri(d, current)) .OrderByDescending(d => d.Uri.ToString()) .ToArray(); - if (domainsAndUris.Any() == false) + if (domainsAndUris.Length == 0) return null; DomainAndUri domainAndUri; @@ -171,13 +163,12 @@ namespace Umbraco.Web.Routing /// The group of domains. /// The uri, or null. /// The domains and their normalized uris, that match the specified uri. - internal static IEnumerable DomainsForUri(IEnumerable domains, Uri current) + internal static IEnumerable DomainsForUri(IEnumerable domains, Uri current) { - var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; return domains .Where(d => d.IsWildcard == false) - .Select(SanitizeForBackwardCompatibility) - .Select(d => new DomainAndUri(d, scheme)) + //.Select(SanitizeForBackwardCompatibility) + .Select(d => new DomainAndUri(d, current)) .OrderByDescending(d => d.Uri.ToString()); } @@ -185,27 +176,6 @@ namespace Umbraco.Web.Routing #region Utilities - /// - /// Sanitize a Domain. - /// - /// The Domain to sanitize. - /// The sanitized domain. - /// This is a _really_ nasty one that should be removed at some point. Some people were - /// using hostnames such as "/en" which happened to work pre-4.10 but really make no sense at - /// all... and 4.10 throws on them, so here we just try to find a way so 4.11 does not throw. - /// But really... no. - private static IDomain SanitizeForBackwardCompatibility(IDomain domain) - { - var context = System.Web.HttpContext.Current; - if (context != null && domain.DomainName.StartsWith("/")) - { - // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri - var authority = context.Request.Url.GetLeftPart(UriPartial.Authority); - domain.DomainName = authority + domain.DomainName; - } - return domain; - } - /// /// Gets a value indicating whether there is another domain defined down in the path to a node under the current domain's root node. /// @@ -214,7 +184,7 @@ namespace Umbraco.Web.Routing /// The current domain root node identifier, or null. /// A value indicating if there is another domain defined down in the path. /// Looks _under_ rootNodeId but not _at_ rootNodeId. - internal static bool ExistsDomainInPath(IEnumerable domains, string path, int? rootNodeId) + internal static bool ExistsDomainInPath(IEnumerable domains, string path, int? rootNodeId) { return FindDomainInPath(domains, path, rootNodeId) != null; } @@ -227,7 +197,7 @@ namespace Umbraco.Web.Routing /// The current domain root node identifier, or null. /// The deepest non-wildcard Domain in the path, or null. /// Looks _under_ rootNodeId but not _at_ rootNodeId. - internal static IDomain FindDomainInPath(IEnumerable domains, string path, int? rootNodeId) + internal static Domain FindDomainInPath(IEnumerable domains, string path, int? rootNodeId) { var stopNodeId = rootNodeId ?? -1; @@ -235,7 +205,7 @@ namespace Umbraco.Web.Routing .Reverse() .Select(int.Parse) .TakeWhile(id => id != stopNodeId) - .Select(id => domains.FirstOrDefault(d => d.RootContentId == id && d.IsWildcard == false)) + .Select(id => domains.FirstOrDefault(d => d.ContentId == id && d.IsWildcard == false)) .SkipWhile(domain => domain == null) .FirstOrDefault(); } @@ -248,7 +218,7 @@ namespace Umbraco.Web.Routing /// The current domain root node identifier, or null. /// The deepest wildcard Domain in the path, or null. /// Looks _under_ rootNodeId but not _at_ rootNodeId. - internal static IDomain FindWildcardDomainInPath(IEnumerable domains, string path, int? rootNodeId) + internal static Domain FindWildcardDomainInPath(IEnumerable domains, string path, int? rootNodeId) { var stopNodeId = rootNodeId ?? -1; @@ -256,7 +226,7 @@ namespace Umbraco.Web.Routing .Reverse() .Select(int.Parse) .TakeWhile(id => id != stopNodeId) - .Select(id => domains.FirstOrDefault(d => d.RootContentId == id && d.IsWildcard)) + .Select(id => domains.FirstOrDefault(d => d.ContentId == id && d.IsWildcard)) .FirstOrDefault(domain => domain != null); } diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 1ed0cf8150..6072137c4d 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -120,9 +120,8 @@ namespace Umbraco.Web.Routing /// internal void OnPreparing() { - var handler = Preparing; - if (handler != null) handler(this, EventArgs.Empty); - _readonlyUri = true; + Preparing?.Invoke(this, EventArgs.Empty); + _readonlyUri = true; } /// @@ -130,8 +129,7 @@ namespace Umbraco.Web.Routing /// internal void OnPrepared() { - var handler = Prepared; - if (handler != null) handler(this, EventArgs.Empty); + Prepared?.Invoke(this, EventArgs.Empty); if (HasPublishedContent == false) Is404 = true; // safety @@ -385,17 +383,11 @@ namespace Umbraco.Web.Routing #region Domain and Culture - [Obsolete("Do not use this property, use the non-legacy UmbracoDomain property instead")] - public Domain Domain - { - get { return new Domain(UmbracoDomain); } - } - //TODO: Should we publicize the setter now that we are using a non-legacy entity?? /// /// Gets or sets the content request's domain. /// - public IDomain UmbracoDomain { get; internal set; } + public DomainAndUri Domain { get; internal set; } /// /// Gets or sets the content request's domain Uri. @@ -408,7 +400,7 @@ namespace Umbraco.Web.Routing /// public bool HasDomain { - get { return UmbracoDomain != null; } + get { return Domain != null; } } private CultureInfo _culture; diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 03a50a4b99..7a780f0d4d 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -1,22 +1,13 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Globalization; using System.IO; -using System.Web.Security; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using Umbraco.Core.Security; - using umbraco; -using umbraco.cms.businesslogic.web; -using umbraco.cms.businesslogic.language; -using umbraco.cms.businesslogic.member; using Umbraco.Core.Services; using Umbraco.Web.Security; using RenderingEngine = Umbraco.Core.RenderingEngine; @@ -24,7 +15,7 @@ using RenderingEngine = Umbraco.Core.RenderingEngine; namespace Umbraco.Web.Routing { internal class PublishedContentRequestEngine - { + { private readonly PublishedContentRequest _pcr; private readonly RoutingContext _routingContext; private readonly IWebRoutingSection _webRoutingSection; @@ -34,19 +25,19 @@ namespace Umbraco.Web.Routing /// /// /// The content request. - public PublishedContentRequestEngine( + public PublishedContentRequestEngine( IWebRoutingSection webRoutingSection, PublishedContentRequest pcr) { if (pcr == null) throw new ArgumentException("pcr is null."); - if (webRoutingSection == null) throw new ArgumentNullException("webRoutingSection"); - + if (webRoutingSection == null) throw new ArgumentNullException(nameof(webRoutingSection)); + _pcr = pcr; _webRoutingSection = webRoutingSection; _routingContext = pcr.RoutingContext; if (_routingContext == null) throw new ArgumentException("pcr.RoutingContext is null."); - + var umbracoContext = _routingContext.UmbracoContext; if (umbracoContext == null) throw new ArgumentException("pcr.RoutingContext.UmbracoContext is null."); if (umbracoContext.RoutingContext != _routingContext) throw new ArgumentException("RoutingContext confusion."); @@ -54,18 +45,11 @@ namespace Umbraco.Web.Routing //if (umbracoContext.PublishedContentRequest != _pcr) throw new ArgumentException("PublishedContentRequest confusion."); } - protected ProfilingLogger ProfilingLogger - { - get { return _routingContext.UmbracoContext.Application.ProfilingLogger; } - } + protected ProfilingLogger ProfilingLogger => _routingContext.UmbracoContext.Application.ProfilingLogger; - protected ServiceContext Services - { - get { return _routingContext.UmbracoContext.Application.Services; } + protected ServiceContext Services => _routingContext.UmbracoContext.Application.Services; - } - - #region Public + #region Public /// /// Prepares the request. @@ -233,7 +217,7 @@ namespace Umbraco.Web.Routing /// /// Finds the site root (if any) matching the http request, and updates the PublishedContentRequest accordingly. - /// + /// /// A value indicating whether a domain was found. internal bool FindDomain() { @@ -244,21 +228,22 @@ namespace Umbraco.Web.Routing ProfilingLogger.Logger.Debug("{0}Uri=\"{1}\"", () => tracePrefix, () => _pcr.Uri); // try to find a domain matching the current request - var domainAndUri = DomainHelper.DomainForUri(Services.DomainService.GetAll(false), _pcr.Uri); + var domainCache = _routingContext.UmbracoContext.Facade.DomainCache; + var domainAndUri = DomainHelper.DomainForUri(domainCache.GetAll(false), _pcr.Uri); - // handle domain - if (domainAndUri != null && domainAndUri.UmbracoDomain.LanguageIsoCode.IsNullOrWhiteSpace() == false) + // handle domain - always has a contentId and a culture + if (domainAndUri != null) { // matching an existing domain ProfilingLogger.Logger.Debug("{0}Matches domain=\"{1}\", rootId={2}, culture=\"{3}\"", () => tracePrefix, - () => domainAndUri.UmbracoDomain.DomainName, - () => domainAndUri.UmbracoDomain.RootContentId, - () => domainAndUri.UmbracoDomain.LanguageIsoCode); + () => domainAndUri.Name, + () => domainAndUri.ContentId, + () => domainAndUri.Culture); - _pcr.UmbracoDomain = domainAndUri.UmbracoDomain; - _pcr.DomainUri = domainAndUri.Uri; - _pcr.Culture = new CultureInfo(domainAndUri.UmbracoDomain.LanguageIsoCode); + _pcr.Domain = domainAndUri; + _pcr.Culture = domainAndUri.Culture; + _pcr.DomainUri = domainAndUri.Uri; // fixme wtf?! // canonical? not implemented at the moment // if (...) @@ -278,7 +263,7 @@ namespace Umbraco.Web.Routing ProfilingLogger.Logger.Debug("{0}Culture=\"{1}\"", () => tracePrefix, () => _pcr.Culture.Name); - return _pcr.UmbracoDomain != null; + return _pcr.Domain != null; } /// @@ -293,14 +278,16 @@ namespace Umbraco.Web.Routing var nodePath = _pcr.PublishedContent.Path; ProfilingLogger.Logger.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => nodePath); - var rootNodeId = _pcr.HasDomain ? _pcr.UmbracoDomain.RootContentId : (int?)null; - var domain = DomainHelper.FindWildcardDomainInPath(Services.DomainService.GetAll(true), nodePath, rootNodeId); + var rootNodeId = _pcr.HasDomain ? _pcr.Domain.ContentId : (int?)null; + var domainCache = _routingContext.UmbracoContext.Facade.DomainCache; + var domain = DomainHelper.FindWildcardDomainInPath(domainCache.GetAll(true), nodePath, rootNodeId); - if (domain != null && domain.LanguageIsoCode.IsNullOrWhiteSpace() == false) + // always has a contentId and a culture + if (domain != null) { - _pcr.Culture = new CultureInfo(domain.LanguageIsoCode); + _pcr.Culture = domain.Culture; ProfilingLogger.Logger.Debug("{0}Got domain on node {1}, set culture to \"{2}\".", () => tracePrefix, - () => domain.RootContentId, () => _pcr.Culture.Name); + () => domain.ContentId, () => _pcr.Culture.Name); } else { @@ -403,8 +390,8 @@ namespace Umbraco.Web.Routing // some finders may implement caching using (ProfilingLogger.DebugDuration( - string.Format("{0}Begin finders", tracePrefix), - string.Format("{0}End finders, {1}", tracePrefix, (_pcr.HasPublishedContent ? "a document was found" : "no document was found")))) + $"{tracePrefix}Begin finders", + $"{tracePrefix}End finders, {(_pcr.HasPublishedContent ? "a document was found" : "no document was found")}")) { if (_routingContext.PublishedContentFinders == null) throw new InvalidOperationException("There is no finder collection."); @@ -429,7 +416,7 @@ namespace Umbraco.Web.Routing { const string tracePrefix = "HandlePublishedContent: "; - // because these might loop, we have to have some sort of infinite loop detection + // because these might loop, we have to have some sort of infinite loop detection int i = 0, j = 0; const int maxLoop = 8; do @@ -494,49 +481,49 @@ namespace Umbraco.Web.Routing if (_pcr.PublishedContent == null) throw new InvalidOperationException("There is no PublishedContent."); - bool redirect = false; + var redirect = false; var internalRedirect = _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.InternalRedirectId); - if (string.IsNullOrWhiteSpace(internalRedirect) == false) - { - ProfilingLogger.Logger.Debug("{0}Found umbracoInternalRedirectId={1}", () => tracePrefix, () => internalRedirect); + if (string.IsNullOrWhiteSpace(internalRedirect)) + return false; - int internalRedirectId; - if (int.TryParse(internalRedirect, out internalRedirectId) == false) - internalRedirectId = -1; + ProfilingLogger.Logger.Debug("{0}Found umbracoInternalRedirectId={1}", () => tracePrefix, () => internalRedirect); - if (internalRedirectId <= 0) - { - // bad redirect - log and display the current page (legacy behavior) - //_pcr.Document = null; // no! that would be to force a 404 - ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect); - } - else if (internalRedirectId == _pcr.PublishedContent.Id) - { - // redirect to self - ProfilingLogger.Logger.Debug("{0}Redirecting to self, ignore", () => tracePrefix); - } - else - { - // redirect to another page - var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); - - if (node != null) - { - _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here - redirect = true; - ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); - } - else - { - ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: no such published document", () => tracePrefix, () => internalRedirectId); - } - } - } + int internalRedirectId; + if (int.TryParse(internalRedirect, out internalRedirectId) == false) + internalRedirectId = -1; - return redirect; + if (internalRedirectId <= 0) + { + // bad redirect - log and display the current page (legacy behavior) + //_pcr.Document = null; // no! that would be to force a 404 + ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect); + } + else if (internalRedirectId == _pcr.PublishedContent.Id) + { + // redirect to self + ProfilingLogger.Logger.Debug("{0}Redirecting to self, ignore", () => tracePrefix); + } + else + { + // redirect to another page + var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); + + if (node != null) + { + _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + redirect = true; + ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); + } + else + { + ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: no such published document", () => tracePrefix, () => internalRedirectId); + } + } + + return redirect; } - + /// /// Ensures that access to current node is permitted. /// @@ -606,10 +593,10 @@ namespace Umbraco.Web.Routing // only if the published content is the initial once, else the alternate template // does not apply // + optionnally, apply the alternate template on internal redirects - var useAltTemplate = _webRoutingSection.DisableAlternativeTemplates == false + var useAltTemplate = _webRoutingSection.DisableAlternativeTemplates == false && (_pcr.IsInitialPublishedContent || (_webRoutingSection.InternalRedirectPreservesTemplate && _pcr.IsInternalRedirectPublishedContent)); - string altTemplate = useAltTemplate + var altTemplate = useAltTemplate ? _routingContext.UmbracoContext.HttpContext.Request[Constants.Conventions.Url.AltTemplate] : null; @@ -702,7 +689,7 @@ namespace Umbraco.Web.Routing if (redirectUrl != "#") _pcr.SetRedirect(redirectUrl); } - + #endregion } } diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index ed3eb5b397..2edfca7a9f 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml; @@ -11,6 +12,8 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; using Umbraco.Web.Cache; using UmbracoExamine; @@ -22,7 +25,7 @@ namespace Umbraco.Web.Search /// public sealed class ExamineEvents : ApplicationEventHandler { - + /// /// Once the application has started we should bind to all events and initialize the providers. /// @@ -30,9 +33,9 @@ namespace Umbraco.Web.Search /// /// /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. - /// + /// protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) - { + { LogHelper.Info("Initializing Examine and binding to business logic events"); //TODO: For now we'll make this true, it means that indexes will be near real time @@ -44,17 +47,17 @@ namespace Umbraco.Web.Search LogHelper.Info("Adding examine event handlers for index providers: {0}", () => registeredProviders); - //don't bind event handlers if we're not suppose to listen + // don't bind event handlers if we're not suppose to listen if (registeredProviders == 0) return; - //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part - // in a load balanced environment. - CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; - + // bind to distributed cache events - this ensures that this logic occurs on ALL servers + // that are taking part in a load balanced environment. + ContentCacheRefresher.CacheUpdated += ContentCacheRefresherUpdated; + MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; + MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; + ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; + var contentIndexer = ExamineManager.Instance.IndexProviderCollection["InternalIndexer"] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -67,12 +70,25 @@ namespace Umbraco.Web.Search } } - static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) + // see: http://issues.umbraco.org/issue/U4-4798 + static void ContentTypeCacheRefresherUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) + { + // fixme wtf? + + //var indexersToUpdate = ExamineManager.Instance.IndexProviderCollection.OfType(); + //foreach (var provider in indexersToUpdate) + //{ + // // fixme - but are we re-indexing? what if a property is removed? + // provider.RefreshIndexerDataFromDataService(); + //} + } + + static void MemberCacheRefresherUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs args) { - switch (e.MessageType) + switch (args.MessageType) { case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)e.MessageObject); + var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)args.MessageObject); if (c1 != null) { ReIndexForMember(c1); @@ -82,10 +98,10 @@ namespace Umbraco.Web.Search // This is triggered when the item is permanently deleted - DeleteIndexForEntity((int)e.MessageObject, false); + DeleteIndexForEntity((int)args.MessageObject, false); break; case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IMember; + var c3 = args.MessageObject as IMember; if (c3 != null) { ReIndexForMember(c3); @@ -95,7 +111,7 @@ namespace Umbraco.Web.Search // This is triggered when the item is permanently deleted - var c4 = e.MessageObject as IMember; + var c4 = args.MessageObject as IMember; if (c4 != null) { DeleteIndexForEntity(c4.Id, false); @@ -109,230 +125,171 @@ namespace Umbraco.Web.Search } } - /// - /// Handles index management for all media events - basically handling saving/copying/trashing/deleting - /// - /// - /// - static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) + static void MediaCacheRefresherUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs args) { - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); - if (c1 != null) + if (args.MessageType != MessageType.RefreshByPayload) + throw new NotSupportedException(); + + var mediaService = ApplicationContext.Current.Services.MediaService; + + foreach (var payload in (MediaCacheRefresher.JsonPayload[]) args.MessageObject) + { + if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) + { + // remove from *all* indexes + DeleteIndexForEntity(payload.Id, false); + } + else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) + { + // ExamineEvents does not support RefreshAll + // just ignore that payload + // so what?! + } + else // RefreshNode or RefreshBranch (maybe trashed) + { + var media = mediaService.GetById(payload.Id); + if (media == null || media.Trashed) { - ReIndexForMedia(c1, c1.Trashed == false); + // gone fishing, remove entirely + DeleteIndexForEntity(payload.Id, false); + continue; } - break; - case MessageType.RemoveById: - var c2 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); - if (c2 != null) + + // just that media + ReIndexForMedia(media, media.Trashed == false); + + // branch + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { - //This is triggered when the item has trashed. - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c2.Id, true); - - //We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForMedia(c2, false); - } - break; - case MessageType.RefreshByJson: - - var jsonPayloads = MediaCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); - if (jsonPayloads.Any()) - { - foreach (var payload in jsonPayloads) + var descendants = mediaService.GetDescendants(media); + foreach (var descendant in descendants) { - switch (payload.Operation) - { - case MediaCacheRefresher.OperationType.Saved: - var media1 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); - if (media1 != null) - { - ReIndexForMedia(media1, media1.Trashed == false); - } - break; - case MediaCacheRefresher.OperationType.Trashed: - - //keep if trashed for indexes supporting unpublished - //(delete the index from all indexes not supporting unpublished content) - - DeleteIndexForEntity(payload.Id, true); - - //We then need to re-index this item for all indexes supporting unpublished content - var media2 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); - if (media2 != null) - { - ReIndexForMedia(media2, false); - } - - break; - case MediaCacheRefresher.OperationType.Deleted: - - //permanently remove from all indexes - - DeleteIndexForEntity(payload.Id, false); - - break; - default: - throw new ArgumentOutOfRangeException(); - } - } + ReIndexForMedia(descendant, descendant.Trashed == false); + } } - - break; - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - case MessageType.RefreshAll: - default: - //We don't support these, these message types will not fire for media - break; - } + } + } } - /// - /// Handles index management for all published content events - basically handling published/unpublished - /// - /// - /// - /// - /// This will execute on all servers taking part in load balancing - /// - static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) - { - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); - if (c1 != null) + static void ContentCacheRefresherUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args) + { + if (args.MessageType != MessageType.RefreshByPayload) + throw new NotSupportedException(); + + var contentService = ApplicationContext.Current.Services.ContentService; + + foreach (var payload in (ContentCacheRefresher.JsonPayload[]) args.MessageObject) + { + if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) + { + // delete content entirely (with descendants) + // false: remove entirely from all indexes + DeleteIndexForEntity(payload.Id, false); + } + else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) + { + // ExamineEvents does not support RefreshAll + // just ignore that payload + // so what?! + } + else // RefreshNode or RefreshBranch (maybe trashed) + { + // don't try to be too clever - refresh entirely + // there has to be race conds in there ;-( + + var content = contentService.GetById(payload.Id); + if (content == null || content.Trashed) { - ReIndexForContent(c1, true); + // gone fishing, remove entirely from all indexes (with descendants) + DeleteIndexForEntity(payload.Id, false); + continue; } - break; - case MessageType.RemoveById: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). - var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); - if (c2 != null) + IContent published = null; + if (content.HasPublishedVersion && ((ContentService)contentService).IsPathPublished(content)) { - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c2.Id, true); - - // We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForContent(c2, false); + published = content.Published + ? content + : contentService.GetByVersion(content.PublishedVersionGuid); } - break; - case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IContent; - if (c3 != null) + + // just that content + ReIndexForContent(content, published); + + // branch + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { - ReIndexForContent(c3, true); - } - break; - case MessageType.RemoveByInstance: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). - - var c4 = e.MessageObject as IContent; - if (c4 != null) - { - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c4.Id, true); - - // We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForContent(c4, false); - } - break; - case MessageType.RefreshAll: - case MessageType.RefreshByJson: - default: - //We don't support these for examine indexing - break; - } - } - - /// - /// Handles index management for all unpublished content events - basically handling saving/copying/deleting - /// - /// - /// - /// - /// This will execute on all servers taking part in load balancing - /// - static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) - { - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.ContentService.GetById((int) e.MessageObject); - if (c1 != null) - { - ReIndexForContent(c1, false); - } - break; - case MessageType.RemoveById: - - // This is triggered when the item is permanently deleted - - DeleteIndexForEntity((int)e.MessageObject, false); - break; - case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IContent; - if (c3 != null) - { - ReIndexForContent(c3, false); - } - break; - case MessageType.RemoveByInstance: - - // This is triggered when the item is permanently deleted - - var c4 = e.MessageObject as IContent; - if (c4 != null) - { - DeleteIndexForEntity(c4.Id, false); - } - break; - case MessageType.RefreshByJson: - - var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); - if (jsonPayloads.Any()) - { - foreach (var payload in jsonPayloads) + var masked = published == null ? null : new List(); + var descendants = contentService.GetDescendants(content); + foreach (var descendant in descendants) { - switch (payload.Operation) + published = null; + if (masked != null) // else everything is masked { - case UnpublishedPageCacheRefresher.OperationType.Deleted: + if (masked.Contains(descendant.ParentId) || descendant.HasPublishedVersion == false) + { + masked.Add(descendant.Id); + } + else + { + published = descendant.Published + ? descendant + : contentService.GetByVersion(descendant.PublishedVersionGuid); + } + } - //permanently remove from all indexes - - DeleteIndexForEntity(payload.Id, false); - - break; - default: - throw new ArgumentOutOfRangeException(); - } - } + ReIndexForContent(descendant, published); + } } + } - break; + // NOTE + // + // DeleteIndexForEntity is handled by UmbracoContentIndexer.DeleteFromIndex() which takes + // care of also deleting the descendants + // + // ReIndexForContent is NOT taking care of descendants so we have to reload everything + // again in order to process the branch - we COULD improve that by just reloading the + // XML from database instead of reloading content & re-serializing! + } + } - case MessageType.RefreshAll: - default: - //We don't support these, these message types will not fire for unpublished content - break; + private static void ReIndexForContent(IContent content, IContent published) + { + if (published != null && content.Version == published.Version) + { + ReIndexForContent(content); // same = both + } + else + { + if (published == null) + { + // remove 'published' - keep 'draft' + DeleteIndexForEntity(content.Id, true); + } + else + { + // index 'published' - don't overwrite 'draft' + ReIndexForContent(published, false); + } + ReIndexForContent(content, true); // index 'draft' } } - + private static void ReIndexForContent(IContent sender, bool? supportUnpublished = null) + { + var xml = sender.ToXml(); + //add an icon attribute to get indexed + xml.Add(new XAttribute("icon", sender.ContentType.Icon)); + + ExamineManager.Instance.ReIndexNode( + xml, IndexTypes.Content, + ExamineManager.Instance.IndexProviderCollection.OfType() + // only for the specified indexers + .Where(x => supportUnpublished.HasValue == false || supportUnpublished.Value == x.SupportUnpublishedContent) + .Where(x => x.EnableDefaultEventHandler)); + } + private static void ReIndexForMember(IMember member) { ExamineManager.Instance.ReIndexNode( @@ -342,14 +299,48 @@ namespace Umbraco.Web.Search .Where(x => x.EnableDefaultEventHandler)); } - /// - /// Event handler to create a lower cased version of the node name, this is so we can support case-insensitive searching and still - /// use the Whitespace Analyzer - /// - /// - /// - - private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) + { + var xml = sender.ToXml(); + //add an icon attribute to get indexed + xml.Add(new XAttribute("icon", sender.ContentType.Icon)); + + ExamineManager.Instance.ReIndexNode( + xml, IndexTypes.Media, + ExamineManager.Instance.IndexProviderCollection.OfType() + // index this item for all indexers if the media is not trashed, otherwise if the item is trashed + // then only index this for indexers supporting unpublished media + .Where(x => isMediaPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); + } + + /// + /// Remove items from any index that doesn't support unpublished content + /// + /// + /// + /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. + /// If false it will delete this from all indexes regardless. + /// + private static void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + ExamineManager.Instance.DeleteFromIndex( + entityId.ToString(CultureInfo.InvariantCulture), + ExamineManager.Instance.IndexProviderCollection.OfType() + // if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, + // otherwise if keepIfUnpublished == false then remove from all indexes + .Where(x => keepIfUnpublished == false || (x is UmbracoContentIndexer && ((UmbracoContentIndexer)x).SupportUnpublishedContent == false)) + .Where(x => x.EnableDefaultEventHandler)); + } + + /// + /// Event handler to create a lower cased version of the node name, this is so we can support case-insensitive searching and still + /// use the Whitespace Analyzer + /// + /// + /// + + private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) { if (e.Fields.Keys.Contains("nodeName")) { @@ -364,70 +355,5 @@ namespace Umbraco.Web.Search )); } } - - private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) - { - var xml = sender.ToXml(); - //add an icon attribute to get indexed - xml.Add(new XAttribute("icon", sender.ContentType.Icon)); - - ExamineManager.Instance.ReIndexNode( - xml, IndexTypes.Media, - ExamineManager.Instance.IndexProviderCollection.OfType() - - //Index this item for all indexers if the media is not trashed, otherwise if the item is trashed - // then only index this for indexers supporting unpublished media - - .Where(x => isMediaPublished || (x.SupportUnpublishedContent)) - .Where(x => x.EnableDefaultEventHandler)); - } - - /// - /// Remove items from any index that doesn't support unpublished content - /// - /// - /// - /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. - /// If false it will delete this from all indexes regardless. - /// - private static void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) - { - ExamineManager.Instance.DeleteFromIndex( - entityId.ToString(CultureInfo.InvariantCulture), - ExamineManager.Instance.IndexProviderCollection.OfType() - - //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, - // otherwise if keepIfUnpublished == false then remove from all indexes - - .Where(x => keepIfUnpublished == false || (x is UmbracoContentIndexer && ((UmbracoContentIndexer)x).SupportUnpublishedContent == false)) - .Where(x => x.EnableDefaultEventHandler)); - } - - /// - /// Re-indexes a content item whether published or not but only indexes them for indexes supporting unpublished content - /// - /// - /// - /// Value indicating whether the item is published or not - /// - private static void ReIndexForContent(IContent sender, bool isContentPublished) - { - var xml = sender.ToXml(); - //add an icon attribute to get indexed - xml.Add(new XAttribute("icon", sender.ContentType.Icon)); - - ExamineManager.Instance.ReIndexNode( - xml, IndexTypes.Content, - ExamineManager.Instance.IndexProviderCollection.OfType() - - //Index this item for all indexers if the content is published, otherwise if the item is not published - // then only index this for indexers supporting unpublished content - - .Where(x => isContentPublished || (x.SupportUnpublishedContent)) - .Where(x => x.EnableDefaultEventHandler)); - } - - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 4f5c14723b..8e47762d93 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -28,6 +28,7 @@ namespace Umbraco.Web.Security private readonly RoleProvider _roleProvider; private readonly ApplicationContext _applicationContext; private readonly HttpContextBase _httpContext; + private readonly IPublishedMemberCache _memberCache; #region Constructors public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext) @@ -37,14 +38,15 @@ namespace Umbraco.Web.Security public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext, MembershipProvider membershipProvider, RoleProvider roleProvider) { - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - if (httpContext == null) throw new ArgumentNullException("httpContext"); - if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); - if (roleProvider == null) throw new ArgumentNullException("roleProvider"); + if (applicationContext == null) throw new ArgumentNullException(nameof(applicationContext)); + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider)); + if (roleProvider == null) throw new ArgumentNullException(nameof(roleProvider)); _applicationContext = applicationContext; _httpContext = httpContext; _membershipProvider = membershipProvider; _roleProvider = roleProvider; + _memberCache = UmbracoContext.Current?.Facade?.MemberCache; // fixme } public MembershipHelper(UmbracoContext umbracoContext) @@ -54,13 +56,14 @@ namespace Umbraco.Web.Security public MembershipHelper(UmbracoContext umbracoContext, MembershipProvider membershipProvider, RoleProvider roleProvider) { - if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); - if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); - if (roleProvider == null) throw new ArgumentNullException("roleProvider"); + if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext)); + if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider)); + if (roleProvider == null) throw new ArgumentNullException(nameof(roleProvider)); _httpContext = umbracoContext.HttpContext; _applicationContext = umbracoContext.Application; _membershipProvider = membershipProvider; _roleProvider = roleProvider; + _memberCache = umbracoContext.Facade.MemberCache; } #endregion @@ -242,66 +245,22 @@ namespace Umbraco.Web.Security public virtual IPublishedContent GetByProviderKey(object key) { - return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( - GetCacheKey("GetByProviderKey", key), () => - { - var provider = _membershipProvider; - if (provider.IsUmbracoMembershipProvider() == false) - { - throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); - } - - var result = _applicationContext.Services.MemberService.GetByProviderKey(key); - return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + return _memberCache.GetByProviderKey(key); } public virtual IPublishedContent GetById(int memberId) { - return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( - GetCacheKey("GetById", memberId), () => - { - var provider = _membershipProvider; - if (provider.IsUmbracoMembershipProvider() == false) - { - throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); - } - - var result = _applicationContext.Services.MemberService.GetById(memberId); - return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + return _memberCache.GetById(memberId); } public virtual IPublishedContent GetByUsername(string username) { - return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( - GetCacheKey("GetByUsername", username), () => - { - var provider = _membershipProvider; - if (provider.IsUmbracoMembershipProvider() == false) - { - throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); - } - - var result = _applicationContext.Services.MemberService.GetByUsername(username); - return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + return _memberCache.GetByUsername(username); } public virtual IPublishedContent GetByEmail(string email) { - return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( - GetCacheKey("GetByEmail", email), () => - { - var provider = _membershipProvider; - if (provider.IsUmbracoMembershipProvider() == false) - { - throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); - } - - var result = _applicationContext.Services.MemberService.GetByEmail(email); - return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + return _memberCache.GetByEmail(email); } /// @@ -315,7 +274,7 @@ namespace Umbraco.Web.Security return null; } var result = GetCurrentPersistedMember(); - return result == null ? null : new MemberPublishedContent(result).CreateModel(); + return result == null ? null : _memberCache.GetByMember(result); } /// diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs index 63c8d5db16..2a853ee022 100644 --- a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs @@ -4,6 +4,8 @@ using Umbraco.Core.Events; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Services; using Umbraco.Core.Configuration; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.XmlPublishedCache; namespace Umbraco.Web.Strategies.Migrations { @@ -27,8 +29,11 @@ namespace Umbraco.Web.Strategies.Migrations if (e.ConfiguredVersion <= target70) { - var mediasvc = (MediaService)ApplicationContext.Current.Services.MediaService; - mediasvc.RebuildXmlStructures(); + // maintain - for backward compatibility? + //var mediasvc = (MediaService)ApplicationContext.Current.Services.MediaService; + //mediasvc.RebuildMediaXml(); + var svc = FacadeServiceResolver.Current.Service as FacadeService; + svc?.RebuildMediaXml(); } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index eee9f834d9..f533cf8e57 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -361,7 +361,7 @@ - + @@ -371,11 +371,33 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -522,10 +544,8 @@ - - @@ -621,7 +641,7 @@ - + @@ -780,7 +800,6 @@ ASPXCodeBehind - True @@ -932,16 +951,9 @@ - + - - - - - - - @@ -1229,9 +1241,6 @@ Code - - Code - Code @@ -1241,7 +1250,6 @@ Code - Code @@ -1349,7 +1357,6 @@ Preview.aspx - xsltVisualize.aspx ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 843b79cb03..7e36e18d7e 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using umbraco.cms.businesslogic.web; using umbraco.presentation.templateControls; using Umbraco.Core.Cache; +using Umbraco.Web.Macros; namespace Umbraco.Web { @@ -117,7 +118,7 @@ namespace Umbraco.Web if (alias == null) throw new ArgumentNullException("alias"); if (umbracoPage == null) throw new ArgumentNullException("umbracoPage"); - var m = macro.GetMacro(alias); + var m = MacroRenderer.GetMacroModel(alias); if (m == null) { throw new KeyNotFoundException("Could not find macro with alias " + alias); @@ -133,10 +134,10 @@ namespace Umbraco.Web /// The parameters. /// The legacy umbraco page object that is required for some macros /// - internal IHtmlString RenderMacro(macro m, IDictionary parameters, page umbracoPage) + internal IHtmlString RenderMacro(MacroModel m, IDictionary parameters, page umbracoPage) { - if (umbracoPage == null) throw new ArgumentNullException("umbracoPage"); - if (m == null) throw new ArgumentNullException("m"); + if (umbracoPage == null) throw new ArgumentNullException(nameof(umbracoPage)); + if (m == null) throw new ArgumentNullException(nameof(m)); if (_umbracoContext.PageId == null) { @@ -153,9 +154,8 @@ namespace Umbraco.Web //NOTE: the value could have html encoded values, so we need to deal with that macroProps.Add(i.Key.ToLowerInvariant(), (i.Value is string) ? HttpUtility.HtmlDecode(i.Value.ToString()) : i.Value); } - var macroControl = m.RenderMacro(macroProps, - umbracoPage.Elements, - _umbracoContext.PageId.Value); + var renderer = new MacroRenderer(ApplicationContext.Current.ProfilingLogger); + var macroControl = renderer.Render(m, umbracoPage.Elements, _umbracoContext.PageId.Value, macroProps).GetAsControl(); string html; if (macroControl is LiteralControl) diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index e657583719..62c5b067e9 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -8,10 +7,6 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; -using umbraco.BusinessLogic; -using umbraco.presentation.preview; -using IOHelper = Umbraco.Core.IO.IOHelper; -using SystemDirectories = Umbraco.Core.IO.SystemDirectories; namespace Umbraco.Web { @@ -25,72 +20,22 @@ namespace Umbraco.Web private bool _replacing; private bool? _previewing; - private readonly Lazy _contentCache; - private readonly Lazy _mediaCache; + private readonly Lazy _facade; /// /// Used if not running in a web application (no real HttpContext) /// [ThreadStatic] - private static UmbracoContext _umbracoContext; + private static UmbracoContext _umbracoContext; // fixme KILL whould use accessor! but accessor uses .Current! catch-22! - #region EnsureContext methods - - #region Obsolete - [Obsolete("Use the method that specifies IUmbracoSettings instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static UmbracoContext EnsureContext( - HttpContextBase httpContext, - ApplicationContext applicationContext, - WebSecurity webSecurity) - { - return EnsureContext(httpContext, applicationContext, webSecurity, false); - } - [Obsolete("Use the method that specifies IUmbracoSettings instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static UmbracoContext EnsureContext( - HttpContextBase httpContext, - ApplicationContext applicationContext) - { - return EnsureContext(httpContext, applicationContext, new WebSecurity(httpContext, applicationContext), false); - } - [Obsolete("Use the method that specifies IUmbracoSettings instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static UmbracoContext EnsureContext( - HttpContextBase httpContext, - ApplicationContext applicationContext, - bool replaceContext) - { - return EnsureContext(httpContext, applicationContext, new WebSecurity(httpContext, applicationContext), replaceContext); - } - [Obsolete("Use the method that specifies IUmbracoSettings instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static UmbracoContext EnsureContext( - HttpContextBase httpContext, - ApplicationContext applicationContext, - WebSecurity webSecurity, - bool replaceContext) - { - return EnsureContext(httpContext, applicationContext, new WebSecurity(httpContext, applicationContext), replaceContext, null); - } - [Obsolete("Use the method that specifies IUmbracoSettings instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static UmbracoContext EnsureContext( - HttpContextBase httpContext, - ApplicationContext applicationContext, - WebSecurity webSecurity, - bool replaceContext, - bool? preview) - { - return EnsureContext(httpContext, applicationContext, webSecurity, UmbracoConfig.For.UmbracoSettings(), UrlProviderResolver.Current.Providers, replaceContext, preview); - } - #endregion + #region Ensure Context /// /// This is a helper method which is called to ensure that the singleton context is created /// /// /// + /// /// /// /// @@ -112,31 +57,31 @@ namespace Umbraco.Web public static UmbracoContext EnsureContext( HttpContextBase httpContext, ApplicationContext applicationContext, + IFacadeService facadeService, WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, IEnumerable urlProviders, bool replaceContext, bool? preview = null) { - if (httpContext == null) throw new ArgumentNullException("httpContext"); - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - if (webSecurity == null) throw new ArgumentNullException("webSecurity"); - if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); - if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (applicationContext == null) throw new ArgumentNullException(nameof(applicationContext)); + if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); + if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); + if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); - //if there's already a singleton, and we're not replacing then there's no need to ensure anything - if (UmbracoContext.Current != null) + // if there is already a current context, + // return if not replacing + // else mark as replacing + if (Current != null) { if (replaceContext == false) - return UmbracoContext.Current; - UmbracoContext.Current._replacing = true; + return Current; + Current._replacing = true; } - var umbracoContext = CreateContext(httpContext, applicationContext, webSecurity, umbracoSettings, urlProviders, preview); - - //assign the singleton - UmbracoContext.Current = umbracoContext; - return UmbracoContext.Current; + // create, assign the singleton, and return + return Current = CreateContext(httpContext, applicationContext, facadeService, webSecurity, umbracoSettings, urlProviders, preview); } /// @@ -144,36 +89,40 @@ namespace Umbraco.Web /// /// /// + /// /// /// - /// + /// /// /// /// A new instance of UmbracoContext - /// + /// public static UmbracoContext CreateContext( HttpContextBase httpContext, ApplicationContext applicationContext, + IFacadeService facadeService, WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, - IEnumerable urlProviders, + IEnumerable urlProviders, bool? preview) { - if (httpContext == null) throw new ArgumentNullException("httpContext"); - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - if (webSecurity == null) throw new ArgumentNullException("webSecurity"); - if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); - if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (applicationContext == null) throw new ArgumentNullException(nameof(applicationContext)); + if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); + if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); + if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); + // create the context var umbracoContext = new UmbracoContext( httpContext, applicationContext, - new Lazy(() => PublishedCachesResolver.Current.Caches, false), + facadeService, webSecurity, preview); - // create the RoutingContext, and assign - var routingContext = new RoutingContext( + // create and assign the RoutingContext, + // note the circular dependency here + umbracoContext.RoutingContext = new RoutingContext( umbracoContext, //TODO: Until the new cache is done we can't really expose these to override/mock @@ -189,52 +138,28 @@ namespace Umbraco.Web urlProviders), false)); - //assign the routing context back - umbracoContext.RoutingContext = routingContext; - return umbracoContext; } - /// - /// Creates a new Umbraco context. - /// - /// - /// - /// The published caches. - /// + /// An HttpContext. + /// An Umbraco application context. + /// A facade service. + /// A web security. /// An optional value overriding detection of preview mode. - internal UmbracoContext( - HttpContextBase httpContext, - ApplicationContext applicationContext, - IPublishedCaches publishedCaches, - WebSecurity webSecurity, - bool? preview = null) - : this(httpContext, applicationContext, new Lazy(() => publishedCaches), webSecurity, preview) - { - } - - /// - /// Creates a new Umbraco context. - /// - /// - /// - /// The published caches. - /// - /// An optional value overriding detection of preview mode. - internal UmbracoContext( - HttpContextBase httpContext, + private UmbracoContext( + HttpContextBase httpContext, ApplicationContext applicationContext, - Lazy publishedCaches, + IFacadeService facadeService, WebSecurity webSecurity, bool? preview = null) { - //This ensures the dispose method is called when the request terminates, though - // we also ensure this happens in the Umbraco module because the UmbracoContext is added to the - // http context items. + // ensure that this instance is disposed when the request terminates, + // though we *also* ensure this happens in the Umbraco module since the + // UmbracoCOntext is added to the HttpContext items. httpContext.DisposeOnPipelineCompleted(this); - if (httpContext == null) throw new ArgumentNullException("httpContext"); - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (applicationContext == null) throw new ArgumentNullException(nameof(applicationContext)); ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); @@ -243,29 +168,20 @@ namespace Umbraco.Web Application = applicationContext; Security = webSecurity; - _contentCache = new Lazy(() => publishedCaches.Value.CreateContextualContentCache(this)); - _mediaCache = new Lazy(() => publishedCaches.Value.CreateContextualMediaCache(this)); - _previewing = preview; - + _facade = new Lazy(() => facadeService.CreateFacade(PreviewToken)); + _previewing = preview; // fixme are we ignoring this entirely?! + // set the urls... - //original request url - //NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this + // NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this // is a work around to being able to access the UmbracoContext during application startup and this will also ensure that people // 'could' still generate URLs during startup BUT any domain driven URL generation will not work because it is NOT possible to get // the current domain during application startup. // see: http://issues.umbraco.org/issue/U4-1890 + // + OriginalRequestUrl = GetRequestFromContext()?.Url ?? new Uri("http://localhost"); + CleanedUmbracoUrl = UriUtility.UriToUmbraco(OriginalRequestUrl); + } - var requestUrl = new Uri("http://localhost"); - var request = GetRequestFromContext(); - if (request != null) - { - requestUrl = request.Url; - } - this.OriginalRequestUrl = requestUrl; - //cleaned request url - this.CleanedUmbracoUrl = UriUtility.UriToUmbraco(this.OriginalRequestUrl); - - } #endregion /// @@ -290,7 +206,7 @@ namespace Umbraco.Web lock (Locker) { //if running in a real HttpContext, this can only be set once - if (System.Web.HttpContext.Current != null && Current != null && !Current._replacing) + if (System.Web.HttpContext.Current != null && Current != null && Current._replacing == false) { throw new ApplicationException("The current UmbracoContext can only be set once during a request."); } @@ -328,12 +244,12 @@ namespace Umbraco.Web /// /// Gets the WebSecurity class /// - public WebSecurity Security { get; private set; } + public WebSecurity Security { get; } /// /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. /// - internal Uri OriginalRequestUrl { get; private set; } + internal Uri OriginalRequestUrl { get; } /// /// Gets the cleaned up url that is handled by Umbraco. @@ -342,30 +258,29 @@ namespace Umbraco.Web internal Uri CleanedUmbracoUrl { get; private set; } /// - /// Gets or sets the published content cache. + /// Gets the facade. /// - public ContextualPublishedContentCache ContentCache - { - get { return _contentCache.Value; } - } + public IFacade Facade => _facade.Value; + + // for unit tests + internal bool HasFacade => _facade.IsValueCreated; /// - /// Gets or sets the published media cache. + /// Gets the published content cache. /// - public ContextualPublishedMediaCache MediaCache - { - get { return _mediaCache.Value; } - } + public IPublishedContentCache ContentCache => Facade.ContentCache; + + /// + /// Gets the published media cache. + /// + public IPublishedMediaCache MediaCache => Facade.MediaCache; /// /// Boolean value indicating whether the current request is a front-end umbraco request /// - public bool IsFrontEndUmbracoRequest - { - get { return PublishedContentRequest != null; } - } + public bool IsFrontEndUmbracoRequest => PublishedContentRequest != null; - /// + /// /// A shortcut to the UmbracoContext's RoutingContext's NiceUrlProvider /// /// @@ -384,17 +299,17 @@ namespace Umbraco.Web /// /// Gets/sets the RoutingContext object /// - public RoutingContext RoutingContext { get; internal set; } + public RoutingContext RoutingContext { get; internal set; } /// /// Gets/sets the PublishedContentRequest object /// - public PublishedContentRequest PublishedContentRequest { get; set; } + public PublishedContentRequest PublishedContentRequest { get; set; } /// /// Exposes the HttpContext for the current request /// - public HttpContextBase HttpContext { get; private set; } + public HttpContextBase HttpContext { get; } /// /// Gets a value indicating whether the request has debugging enabled @@ -406,8 +321,10 @@ namespace Umbraco.Web { var request = GetRequestFromContext(); //NOTE: the request can be null during app startup! - return GlobalSettings.DebugMode && request != null - && (!string.IsNullOrEmpty(request["umbdebugshowtrace"]) || !string.IsNullOrEmpty(request["umbdebug"])); + return GlobalSettings.DebugMode + && request != null + && (string.IsNullOrEmpty(request["umbdebugshowtrace"]) == false + || string.IsNullOrEmpty(request["umbdebug"]) == false); } } @@ -434,36 +351,41 @@ namespace Umbraco.Web } } } - + /// /// Determines whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) /// /// Can be internally set by the RTE macro rendering to render macros in the appropriate mode. + // fixme - that's bad, RTE macros should then create their own facade?! they don't have a preview token! public bool InPreviewMode { - get { return _previewing ?? (_previewing = DetectInPreviewModeFromRequest()).Value; } - set { _previewing = value; } + get { return _previewing ?? (_previewing = (PreviewToken.IsNullOrWhiteSpace() == false)).Value; } + set { _previewing = value; } } - private bool DetectInPreviewModeFromRequest() + private string PreviewToken { - var request = GetRequestFromContext(); - if (request == null || request.Url == null) - return false; - - return - HttpContext.Request.HasPreviewCookie() - && request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false - && Security.CurrentUser != null; // has user + get + { + var request = GetRequestFromContext(); + if (request?.Url == null) + return null; + + if (request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) return null; + if (Security.CurrentUser == null) return null; + + var previewToken = request.GetPreviewCookieValue(); // may be null or empty + return previewToken.IsNullOrWhiteSpace() ? null : previewToken; + } } - + private HttpRequestBase GetRequestFromContext() { try { return HttpContext.Request; } - catch (System.Web.HttpException) + catch (HttpException) { return null; } @@ -476,6 +398,12 @@ namespace Umbraco.Web //If not running in a web ctx, ensure the thread based instance is nulled _umbracoContext = null; + + // help caches release resources + // (but don't create caches just to dispose them) + // context is not multi-threaded + if (_facade.IsValueCreated) + _facade.Value.DisposeIfDisposable(); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 04346763ce..e578edfe86 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -22,6 +22,7 @@ using Umbraco.Web.Security; using umbraco; using Umbraco.Core.Collections; using Umbraco.Core.Sync; +using Umbraco.Web.PublishedCache; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ObjectExtensions = Umbraco.Core.ObjectExtensions; using RenderingEngine = Umbraco.Core.RenderingEngine; @@ -63,11 +64,13 @@ namespace Umbraco.Web // create the UmbracoContext singleton, one per request, and assign // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) - UmbracoContext.EnsureContext( - httpContext, - ApplicationContext.Current, - new WebSecurity(httpContext, ApplicationContext.Current), - true); + UmbracoContext.EnsureContext( + httpContext, ApplicationContext.Current, + FacadeServiceResolver.Current.Service, + new WebSecurity(httpContext, ApplicationContext.Current), + UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, + true); } /// diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs index dda19f9ff9..3836d90fd7 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs @@ -20,6 +20,8 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; using IModelBinder = System.Web.Http.ModelBinding.IModelBinder; @@ -98,12 +100,16 @@ namespace Umbraco.Web.WebApi.Binders { var request = actionContext.Request; - //IMPORTANT!!! We need to ensure the umbraco context here because this is running in an async thread + // IMPORTANT!!! We need to ensure the umbraco context here because this is running in an async thread var httpContext = (HttpContextBase) request.Properties["MS_HttpContext"]; + UmbracoContext.EnsureContext( - httpContext, - ApplicationContext.Current, - new WebSecurity(httpContext, ApplicationContext.Current)); + httpContext, ApplicationContext.Current, + FacadeServiceResolver.Current.Service, + new WebSecurity(httpContext, ApplicationContext.Current), + Core.Configuration.UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, + false); var content = request.Content; diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 39bd43fd18..a58c15548b 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -11,15 +11,12 @@ using System.Web.Mvc; using System.Web.Routing; using ClientDependency.Core.Config; using Examine; -using Examine.Config; using LightInject; -using Examine.Providers; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Logging; using Umbraco.Core.Macros; -using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Sync; @@ -27,7 +24,6 @@ using Umbraco.Web.Dictionary; using Umbraco.Web.Install; using Umbraco.Web.Media; using Umbraco.Web.Media.ThumbnailProviders; -using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -41,6 +37,9 @@ using Umbraco.Core.Services; using Umbraco.Web.Services; using Umbraco.Web.Editors; using Umbraco.Core.DependencyInjection; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services.Changes; +using Umbraco.Web.Cache; using Umbraco.Web.DependencyInjection; using Umbraco.Web._Legacy.Actions; using Action = System.Action; @@ -56,17 +55,13 @@ namespace Umbraco.Web /// public class WebBootManager : CoreBootManager { - private readonly bool _isForTesting; - //TODO: Fix this - we need to manually perform re-indexing on startup when necessary Examine lib no longer does this //NOTE: see the Initialize method for what this is used for //private static readonly List IndexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication) - { - _isForTesting = false; - } + { } /// /// Constructor for unit tests, ensures some resolvers are not initialized @@ -76,9 +71,7 @@ namespace Umbraco.Web /// internal WebBootManager(UmbracoApplicationBase umbracoApplication, ProfilingLogger logger, bool isForTesting) : base(umbracoApplication, logger) - { - _isForTesting = isForTesting; - } + { } /// /// Initialize objects before anything during the boot cycle happens @@ -135,12 +128,14 @@ namespace Umbraco.Web { base.FreezeResolution(); + IFacadeService facadeService; + //before we do anything, we'll ensure the umbraco context //see: http://issues.umbraco.org/issue/U4-1717 var httpContext = new HttpContextWrapper(UmbracoApplication.Context); UmbracoContext.EnsureContext( - httpContext, - ApplicationContext, + httpContext, ApplicationContext, + FacadeServiceResolver.Current.Service, new WebSecurity(httpContext, ApplicationContext), UmbracoConfig.For.UmbracoSettings(), UrlProviderResolver.Current.Providers, @@ -335,11 +330,15 @@ namespace Umbraco.Web //no need to declare as per request, it's lifetime is already managed as a singleton container.Register(factory => new HttpContextWrapper(HttpContext.Current)); container.RegisterSingleton(); - container.RegisterSingleton(factory => new PublishedContentCache()); - container.RegisterSingleton(); + + // register the facade service + container.RegisterSingleton(factory => new FacadeService( + factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance().RequestCache)); //no need to declare as per request, currently we manage it's lifetime as the singleton - container.Register(factory => UmbracoContext.Current); + container.Register(factory => UmbracoContext.Current); container.RegisterSingleton(); //Replace services: @@ -424,7 +423,18 @@ namespace Umbraco.Web InitializingCallbacks = new Action[] { //rebuild the xml cache file if the server is not synced - () => global::umbraco.content.Instance.RefreshContentFromDatabase(), + () => + { + // rebuild the facade caches entirely, if the server is not synced + // this is equivalent to DistributedCache RefreshAllFacade but local only + // (we really should have a way to reuse RefreshAllFacade... locally) + // note: refresh all content & media caches does refresh content types too + IFacadeService svc = FacadeServiceResolver.Current.Service; + bool ignored1, ignored2; + svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainCacheRefresher.ChangeTypes.RefreshAll) }); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1, out ignored2); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1); + }, //rebuild indexes if the server is not synced // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific // indexes then they can adjust this logic themselves. @@ -452,11 +462,11 @@ namespace Umbraco.Web // (the limited one, defined in Core, is there for tests) PropertyValueConvertersResolver.Current.RemoveType(); // same for other converters - PropertyValueConvertersResolver.Current.RemoveType(); - PropertyValueConvertersResolver.Current.RemoveType(); - PropertyValueConvertersResolver.Current.RemoveType(); + PropertyValueConvertersResolver.Current.RemoveType(); + PropertyValueConvertersResolver.Current.RemoveType(); + PropertyValueConvertersResolver.Current.RemoveType(); - PublishedCachesResolver.Current = new PublishedCachesResolver(Container, typeof(PublishedCaches)); + FacadeServiceResolver.Current = new FacadeServiceResolver(Container); FilteredControllerFactoriesResolver.Current = new FilteredControllerFactoriesResolver( ServiceProvider, ProfilingLogger.Logger, @@ -491,9 +501,6 @@ namespace Umbraco.Web SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(Container, typeof(SiteDomainHelper)); - // ain't that a bit dirty? YES - PublishedContentCache.UnitTesting = _isForTesting; - ThumbnailProvidersResolver.Current = new ThumbnailProvidersResolver( Container, ProfilingLogger.Logger, PluginManager.ResolveThumbnailProviders()); diff --git a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs index 8169a44284..e47aa4d4b9 100644 --- a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs +++ b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs @@ -1,6 +1,5 @@ using System; using System.Web.Mvc; -using umbraco; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Web.Mvc; @@ -13,28 +12,25 @@ namespace Umbraco.Web.WebServices [AdminTokenAuthorize] public class ScheduledPublishController : UmbracoController { - private static bool _isPublishingRunning = false; + private static bool _isPublishingRunning; + private static readonly object Locker = new object(); [HttpPost] public JsonResult Index() { - if (_isPublishingRunning) - return null; - _isPublishingRunning = true; + lock (Locker) + { + if (_isPublishingRunning) + return null; + _isPublishingRunning = true; + } try { - // DO not run publishing if content is re-loading - if (content.Instance.isInitializing == false) - { - Services.ContentService.WithResult().PerformScheduledPublish(); - } - - return Json(new - { - success = true - }); - + // ensure we have everything we need + if (ApplicationContext.IsReady == false) return null; + Services.ContentService.WithResult().PerformScheduledPublish(); + return Json(new { success = true }); } catch (Exception ee) { @@ -50,7 +46,10 @@ namespace Umbraco.Web.WebServices } finally { - _isPublishingRunning = false; + lock (Locker) + { + _isPublishingRunning = false; + } } } } diff --git a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs index 589801a829..94f5428f43 100644 --- a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs +++ b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs @@ -4,6 +4,8 @@ using NPoco; using Umbraco.Core; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; @@ -12,75 +14,53 @@ namespace Umbraco.Web.WebServices [ValidateAngularAntiForgeryToken] public class XmlDataIntegrityController : UmbracoAuthorizedApiController { + private readonly FacadeService _facadeService; + + public XmlDataIntegrityController(IFacadeService facadeService) + { + _facadeService = facadeService as FacadeService; + if (_facadeService == null) + throw new NotSupportedException("Unsupported IFacadeService, only the Xml one is supported."); + + } + [HttpPost] public bool FixContentXmlTable() { - Services.ContentService.RebuildXmlStructures(); - return CheckContentXmlTable(); + _facadeService.RebuildContentAndPreviewXml(); + return _facadeService.VerifyContentAndPreviewXml(); } [HttpPost] public bool FixMediaXmlTable() { - Services.MediaService.RebuildXmlStructures(); - return CheckMediaXmlTable(); + _facadeService.RebuildMediaXml(); + return _facadeService.VerifyMediaXml(); } [HttpPost] public bool FixMembersXmlTable() { - Services.MemberService.RebuildXmlStructures(); - return CheckMembersXmlTable(); + _facadeService.RebuildMemberXml(); + return _facadeService.VerifyMemberXml(); } [HttpGet] public bool CheckContentXmlTable() { - var totalPublished = Services.ContentService.CountPublished(); - - var subQuery = DatabaseContext.Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var totalXml = DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM (" + subQuery.SQL + ") as tmp"); - - return totalXml == totalPublished; + return _facadeService.VerifyContentAndPreviewXml(); } - + [HttpGet] public bool CheckMediaXmlTable() { - var total = Services.MediaService.Count(); - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = DatabaseContext.Sql() - .SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - var totalXml = DatabaseContext.Database.ExecuteScalar(subQuery); - - return totalXml == total; + return _facadeService.VerifyMediaXml(); } [HttpGet] public bool CheckMembersXmlTable() { - var total = Services.MemberService.Count(); - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = DatabaseContext.Sql() - .SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - var totalXml = DatabaseContext.Database.ExecuteScalar(subQuery); - - return totalXml == total; + return _facadeService.VerifyMemberXml(); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs deleted file mode 100644 index 99120deee0..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ /dev/null @@ -1,1209 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Xml; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.web; -using umbraco.DataLayer; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Profiling; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Core.Xml; -using Umbraco.Web; -using Umbraco.Web.PublishedCache.XmlPublishedCache; -using Umbraco.Web.Scheduling; -using File = System.IO.File; -using Task = System.Threading.Tasks.Task; - -namespace umbraco -{ - /// - /// Represents the Xml storage for the Xml published cache. - /// - public class content - { - private XmlCacheFilePersister _persisterTask; - - private volatile bool _released; - - #region Constructors - - private content() - { - if (SyncToXmlFile) - { - var logger = LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(); - var profingLogger = new ProfilingLogger( - logger, - ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger)); - - // prepare the persister task - // there's always be one task keeping a ref to the runner - // so it's safe to just create it as a local var here - var runner = new BackgroundTaskRunner("XmlCacheFilePersister", new BackgroundTaskRunnerOptions - { - LongRunning = true, - KeepAlive = true, - Hosted = false // main domain will take care of stopping the runner (see below) - }, logger); - - // create (and add to runner) - _persisterTask = new XmlCacheFilePersister(runner, this, profingLogger); - - var registered = ApplicationContext.Current.MainDom.Register( - null, - () => - { - // once released, the cache still works but does not write to file anymore, - // which is OK with database server messenger but will cause data loss with - // another messenger... - - runner.Shutdown(false, true); // wait until flushed - _released = true; - }); - - // failed to become the main domain, we will never use the file - if (registered == false) - runner.Shutdown(false, true); - - _released = (registered == false); - } - - // initialize content - populate the cache - using (var safeXml = GetSafeXmlWriter(false)) - { - bool registerXmlChange; - - // if we don't use the file then LoadXmlLocked will not even - // read from the file and will go straight to database - LoadXmlLocked(safeXml, out registerXmlChange); - // if we use the file and registerXmlChange is true this will - // write to file, else it will not - safeXml.Commit(registerXmlChange); - } - } - - #endregion - - #region Singleton - - private static readonly Lazy LazyInstance = new Lazy(() => new content()); - - public static content Instance - { - get - { - return LazyInstance.Value; - } - } - - #endregion - - #region Legacy & Stuff - - // sync database access - // (not refactoring that part at the moment) - private static readonly object DbReadSyncLock = new object(); - - private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; - private static string _umbracoXmlDiskCacheFileName = string.Empty; - private volatile XmlDocument _xmlContent; - - /// - /// Gets the path of the umbraco XML disk cache file. - /// - /// The name of the umbraco XML disk cache file. - public static string GetUmbracoXmlDiskFileName() - { - if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) - { - _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); - } - return _umbracoXmlDiskCacheFileName; - } - - [Obsolete("Use the safer static GetUmbracoXmlDiskFileName() method instead to retrieve this value")] - public string UmbracoXmlDiskCacheFileName - { - get { return GetUmbracoXmlDiskFileName(); } - set { _umbracoXmlDiskCacheFileName = value; } - } - - //NOTE: We CANNOT use this for a double check lock because it is a property, not a field and to do double - // check locking in c# you MUST have a volatile field. Even thoug this wraps a volatile field it will still - // not work as expected for a double check lock because properties are treated differently in the clr. - public virtual bool isInitializing - { - get { return _xmlContent == null; } - } - - #endregion - - #region Public Methods - - /// - /// Load content from database and replaces active content when done. - /// - public virtual void RefreshContentFromDatabase() - { - using (var safeXml = GetSafeXmlWriter()) - { - safeXml.Xml = LoadContentFromDatabase(); - } - } - - /// - /// Used by all overloaded publish methods to do the actual "noderepresentation to xml" - /// - /// - /// - /// - public static XmlDocument PublishNodeDo(Document d, XmlDocument xmlContentCopy, bool updateSitemapProvider) - { - // check if document *is* published, it could be unpublished by an event - if (d.Published) - { - var parentId = d.Level == 1 ? -1 : d.ParentId; - - // fix sortOrder - see note in UpdateSortOrder - var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); - var attr = ((XmlElement)node).GetAttributeNode("sortOrder"); - attr.Value = d.sortOrder.ToString(); - xmlContentCopy = GetAddOrUpdateXmlNode(xmlContentCopy, d.Id, d.Level, parentId, node); - - } - - return xmlContentCopy; - } - - private static XmlNode GetPreviewOrPublishedNode(Document d, XmlDocument xmlContentCopy, bool isPreview) - { - var contentItem = d.ContentEntity; - var services = ApplicationContext.Current.Services; - - if (isPreview) - { - var xml = services.ContentService.GetContentPreviewXml(contentItem.Id, contentItem.Version); - return xml.GetXmlNode(xmlContentCopy); - } - else - { - var xml = services.ContentService.GetContentXml(contentItem.Id); - return xml.GetXmlNode(xmlContentCopy); - } - } - - /// - /// Sorts the documents. - /// - /// The parent node identifier. - public void SortNodes(int parentId) - { - var childNodesXPath = "./* [@id]"; - - using (var safeXml = GetSafeXmlWriter(false)) - { - var parentNode = parentId == -1 - ? safeXml.Xml.DocumentElement - : safeXml.Xml.GetElementById(parentId.ToString(CultureInfo.InvariantCulture)); - - if (parentNode == null) return; - - var sorted = XmlHelper.SortNodesIfNeeded( - parentNode, - childNodesXPath, - x => x.AttributeValue("sortOrder")); - - if (sorted == false) return; - - safeXml.Commit(); - } - } - - /// - /// Updates the document cache. - /// - /// The page id. - public virtual void UpdateDocumentCache(int pageId) - { - var d = new Document(pageId); - UpdateDocumentCache(d); - } - - /// - /// Updates the document cache. - /// - /// The d. - public virtual void UpdateDocumentCache(Document d) - { - var e = new DocumentCacheEventArgs(); - - // lock the xml cache so no other thread can write to it at the same time - // note that some threads could read from it while we hold the lock, though - using (var safeXml = GetSafeXmlWriter()) - { - safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); - } - - ClearContextCache(); - - var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(cachedFieldKeyStart); - - FireAfterUpdateDocumentCache(d, e); - } - - internal virtual void UpdateSortOrder(int contentId) - { - var content = ApplicationContext.Current.Services.ContentService.GetById(contentId); - if (content == null) return; - UpdateSortOrder(content); - } - - internal virtual void UpdateSortOrder(IContent c) - { - if (c == null) throw new ArgumentNullException("c"); - - // the XML in database is updated only when content is published, and then - // it contains the sortOrder value at the time the XML was generated. when - // a document with unpublished changes is sorted, then it is simply saved - // (see ContentService) and so the sortOrder has changed but the XML has - // not been updated accordingly. - - // this updates the published cache to take care of the situation - // without ContentService having to ... what exactly? - - // no need to do it if the content is published without unpublished changes, - // though, because in that case the XML will get re-generated with the - // correct sort order. - if (c.Published) - return; - - using (var safeXml = GetSafeXmlWriter(false)) - { - var node = safeXml.Xml.GetElementById(c.Id.ToString(CultureInfo.InvariantCulture)); - if (node == null) return; - var attr = node.GetAttributeNode("sortOrder"); - if (attr == null) return; - var sortOrder = c.SortOrder.ToString(CultureInfo.InvariantCulture); - if (attr.Value == sortOrder) return; - - // only if node was actually modified - attr.Value = sortOrder; - - safeXml.Commit(); - } - } - - /// - /// Updates the document cache for multiple documents - /// - /// The documents. - [Obsolete("This is not used and will be removed from the codebase in future versions")] - public virtual void UpdateDocumentCache(List Documents) - { - // We need to lock content cache here, because we cannot allow other threads - // making changes at the same time, they need to be queued - int parentid = Documents[0].Id; - - - using (var safeXml = GetSafeXmlWriter()) - { - foreach (Document d in Documents) - { - safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); - } - } - - ClearContextCache(); - } - - public virtual void ClearDocumentCache(int documentId) - { - var e = new DocumentCacheEventArgs(); - // Get the document - Document d; - try - { - d = new Document(documentId); - } - catch - { - // if we need the document to remove it... this cannot be LB?! - // shortcut everything here - ClearDocumentXmlCache(documentId); - return; - } - ClearDocumentCache(d); - FireAfterClearDocumentCache(d, e); - } - - /// - /// Clears the document cache and removes the document from the xml db cache. - /// This means the node gets unpublished from the website. - /// - /// The document - internal void ClearDocumentCache(Document doc) - { - var e = new DocumentCacheEventArgs(); - XmlNode x; - - // remove from xml db cache - doc.XmlRemoveFromDB(); - - // clear xml cache - ClearDocumentXmlCache(doc.Id); - - ClearContextCache(); - - FireAfterClearDocumentCache(doc, e); - } - - internal void ClearDocumentXmlCache(int id) - { - // We need to lock content cache here, because we cannot allow other threads - // making changes at the same time, they need to be queued - using (var safeXml = GetSafeXmlReader()) - { - // Check if node present, before cloning - var x = safeXml.Xml.GetElementById(id.ToString()); - if (x == null) - return; - - safeXml.UpgradeToWriter(false); - - // Find the document in the xml cache - x = safeXml.Xml.GetElementById(id.ToString()); - if (x != null) - { - // The document already exists in cache, so repopulate it - x.ParentNode.RemoveChild(x); - safeXml.Commit(); - } - } - } - - /// - /// Unpublishes the node. - /// - /// The document id. - [Obsolete("Please use: umbraco.content.ClearDocumentCache", true)] - public virtual void UnPublishNode(int documentId) - { - ClearDocumentCache(documentId); - } - - #endregion - - #region Protected & Private methods - - /// - /// Clear HTTPContext cache if any - /// - private void ClearContextCache() - { - // If running in a context very important to reset context cache orelse new nodes are missing - if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null && UmbracoContext.Current.HttpContext.Items.Contains(XmlContextContentItemKey)) - UmbracoContext.Current.HttpContext.Items.Remove(XmlContextContentItemKey); - } - - /// - /// Load content from database - /// - private XmlDocument LoadContentFromDatabase() - { - try - { - // Try to log to the DB - LogHelper.Info("Loading content from database..."); - - var hierarchy = new Dictionary>(); - var nodeIndex = new Dictionary(); - - try - { - LogHelper.Debug("Republishing starting"); - - lock (DbReadSyncLock) - { - - // Lets cache the DTD to save on the DB hit on the subsequent use - string dtd = ApplicationContext.Current.Services.ContentTypeService.GetDtd(); - - // Prepare an XmlDocument with an appropriate inline DTD to match - // the expected content - var xmlDoc = new XmlDocument(); - InitializeXml(xmlDoc, dtd); - - // Esben Carlsen: At some point we really need to put all data access into to a tier of its own. - // CLN - added checks that document xml is for a document that is actually published. - string sql = - @"select umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, cmsContentXml.xml from umbracoNode -inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type -where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.level, umbracoNode.sortOrder"; - - - foreach (var dr in ApplicationContext.Current.DatabaseContext.Database.Query(sql, new { type = new Guid(Constants.ObjectTypes.Document)})) - { - int currentId = dr.id; - int parentId = dr.parentId; - string xml = dr.xml; - - // fix sortOrder - see notes in UpdateSortOrder - var tmp = new XmlDocument(); - tmp.LoadXml(xml); - var attr = tmp.DocumentElement.GetAttributeNode("sortOrder"); - attr.Value = dr.sortOrder.ToString(); - xml = tmp.InnerXml; - - // check if a listener has canceled the event - // and parse it into a DOM node - xmlDoc.LoadXml(xml); - XmlNode node = xmlDoc.FirstChild; - nodeIndex.Add(currentId, node); - - // verify if either of the handlers canceled the children to load - // Build the content hierarchy - List children; - if (!hierarchy.TryGetValue(parentId, out children)) - { - // No children for this parent, so add one - children = new List(); - hierarchy.Add(parentId, children); - } - children.Add(currentId); - } - - LogHelper.Debug("Xml Pages loaded"); - - try - { - // If we got to here we must have successfully retrieved the content from the DB so - // we can safely initialise and compose the final content DOM. - // Note: We are reusing the XmlDocument used to create the xml nodes above so - // we don't have to import them into a new XmlDocument - - // Initialise the document ready for the final composition of content - InitializeXml(xmlDoc, dtd); - - // Start building the content tree recursively from the root (-1) node - GenerateXmlDocument(hierarchy, nodeIndex, -1, xmlDoc.DocumentElement); - - LogHelper.Debug("Done republishing Xml Index"); - - return xmlDoc; - } - catch (Exception ee) - { - LogHelper.Error("Error while generating XmlDocument from database", ee); - } - } - } - catch (OutOfMemoryException ee) - { - LogHelper.Error(string.Format("Error Republishing: Out Of Memory. Parents: {0}, Nodes: {1}", hierarchy.Count, nodeIndex.Count), ee); - } - catch (Exception ee) - { - LogHelper.Error("Error Republishing", ee); - } - } - catch (Exception ee) - { - LogHelper.Error("Error Republishing", ee); - } - - // An error of some sort must have stopped us from successfully generating - // the content tree, so lets return null signifying there is no content available - return null; - } - - private static void GenerateXmlDocument(IDictionary> hierarchy, - IDictionary nodeIndex, int parentId, XmlNode parentNode) - { - List children; - - if (hierarchy.TryGetValue(parentId, out children)) - { - XmlNode childContainer = parentNode; - - - foreach (int childId in children) - { - XmlNode childNode = nodeIndex[childId]; - - parentNode.AppendChild(childNode); - - // Recursively build the content tree under the current child - GenerateXmlDocument(hierarchy, nodeIndex, childId, childNode); - } - } - } - - - #endregion - - #region Configuration - - // gathering configuration options here to document what they mean - - private readonly bool _xmlFileEnabled = true; - - // whether the disk cache is enabled - private bool XmlFileEnabled - { - get { return _xmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled; } - } - - // whether the disk cache is enabled and to update the disk cache when xml changes - private bool SyncToXmlFile - { - get { return XmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache; } - } - - // whether the disk cache is enabled and to reload from disk cache if it changes - private bool SyncFromXmlFile - { - get { return XmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.XmlContentCheckForDiskChanges; } - } - - - // whether to keep version of everything (incl. medias & members) in cmsPreviewXml - // for audit purposes - false by default, not in umbracoSettings.config - // whether to... no idea what that one does - // it is false by default and not in UmbracoSettings.config anymore - ignoring - /* - private static bool GlobalPreviewStorageEnabled - { - get { return UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled; } - } - */ - - // ensures config is valid - - #endregion - - #region Xml - - private readonly AsyncLock _xmlLock = new AsyncLock(); // protects _xml - - /// - /// Get content. First call to this property will initialize xmldoc - /// subsequent calls will be blocked until initialization is done - /// Further we cache (in context) xmlContent for each request to ensure that - /// we always have the same XmlDoc throughout the whole request. - /// - public virtual XmlDocument XmlContent - { - get - { - if (UmbracoContext.Current == null || UmbracoContext.Current.HttpContext == null) - return XmlContentInternal; - var content = UmbracoContext.Current.HttpContext.Items[XmlContextContentItemKey] as XmlDocument; - if (content == null) - { - content = XmlContentInternal; - UmbracoContext.Current.HttpContext.Items[XmlContextContentItemKey] = content; - } - return content; - } - } - - [Obsolete("Please use: content.Instance.XmlContent")] - public static XmlDocument xmlContent - { - get { return Instance.XmlContent; } - } - - // to be used by content.Instance - protected internal virtual XmlDocument XmlContentInternal - { - get - { - ReloadXmlFromFileIfChanged(); - return _xmlContent; - } - } - - // assumes xml lock - private void SetXmlLocked(XmlDocument xml, bool registerXmlChange) - { - // this is the ONLY place where we write to _xmlContent - _xmlContent = xml; - - if (registerXmlChange == false || SyncToXmlFile == false) - return; - - //_lastXmlChange = DateTime.UtcNow; - _persisterTask = _persisterTask.Touch(); // _persisterTask != null because SyncToXmlFile == true - } - - private static XmlDocument Clone(XmlDocument xmlDoc) - { - return xmlDoc == null ? null : (XmlDocument)xmlDoc.CloneNode(true); - } - - private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) - { - string subset = null; - - // get current doctype - var n = xml.FirstChild; - while (n.NodeType != XmlNodeType.DocumentType && n.NextSibling != null) - n = n.NextSibling; - if (n.NodeType == XmlNodeType.DocumentType) - subset = ((XmlDocumentType)n).InternalSubset; - - // ensure it contains the content type - if (subset != null && subset.Contains(string.Format("", contentTypeAlias))) - return xml; - - // alas, that does not work, replacing a doctype is ignored and GetElementById fails - // - //// remove current doctype, set new doctype - //xml.RemoveChild(n); - //subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); - //var doctype = xml.CreateDocumentType("root", null, null, subset); - //xml.InsertAfter(doctype, xml.FirstChild); - - var xml2 = new XmlDocument(); - subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); - var doctype = xml2.CreateDocumentType("root", null, null, subset); - xml2.AppendChild(doctype); - xml2.AppendChild(xml2.ImportNode(xml.DocumentElement, true)); - return xml2; - } - - private static void InitializeXml(XmlDocument xml, string dtd) - { - // prime the xml document with an inline dtd and a root element - xml.LoadXml(String.Format("{0}{1}{0}", - Environment.NewLine, dtd)); - } - - // try to load from file, otherwise database - // assumes xml lock (file is always locked) - private void LoadXmlLocked(SafeXmlReaderWriter safeXml, out bool registerXmlChange) - { - LogHelper.Debug("Loading Xml..."); - - // try to get it from the file - if (XmlFileEnabled && (safeXml.Xml = LoadXmlFromFile()) != null) - { - registerXmlChange = false; // loaded from disk, do NOT write back to disk! - return; - } - - // get it from the database, and register - safeXml.Xml = LoadContentFromDatabase(); - registerXmlChange = true; - } - - // NOTE - // - this is NOT a reader/writer lock and each lock is exclusive - // - these locks are NOT reentrant / recursive - - // gets a locked safe read access to the main xml - private SafeXmlReaderWriter GetSafeXmlReader() - { - var releaser = _xmlLock.Lock(); - return SafeXmlReaderWriter.GetReader(this, releaser); - } - - // gets a locked safe write access to the main xml (cloned) - private SafeXmlReaderWriter GetSafeXmlWriter(bool auto = true) - { - var releaser = _xmlLock.Lock(); - return SafeXmlReaderWriter.GetWriter(this, releaser, auto); - } - - private class SafeXmlReaderWriter : IDisposable - { - private readonly content _instance; - private IDisposable _releaser; - private bool _isWriter; - private bool _auto; - private bool _committed; - private XmlDocument _xml; - - private SafeXmlReaderWriter(content instance, IDisposable releaser, bool isWriter, bool auto) - { - _instance = instance; - _releaser = releaser; - _isWriter = isWriter; - _auto = auto; - - // cloning for writer is not an option anymore (see XmlIsImmutable) - _xml = _isWriter ? Clone(instance._xmlContent) : instance._xmlContent; - } - - public static SafeXmlReaderWriter GetReader(content instance, IDisposable releaser) - { - return new SafeXmlReaderWriter(instance, releaser, false, false); - } - - public static SafeXmlReaderWriter GetWriter(content instance, IDisposable releaser, bool auto) - { - return new SafeXmlReaderWriter(instance, releaser, true, auto); - } - - public void UpgradeToWriter(bool auto) - { - if (_isWriter) - throw new InvalidOperationException("Already writing."); - _isWriter = true; - _auto = auto; - _xml = Clone(_xml); // cloning for writer is not an option anymore (see XmlIsImmutable) - } - - public XmlDocument Xml - { - get - { - return _xml; - } - set - { - if (_isWriter == false) - throw new InvalidOperationException("Not writing."); - _xml = value; - } - } - - // registerXmlChange indicates whether to do what should be done when Xml changes, - // that is, to request that the file be written to disk - something we don't want - // to do if we're committing Xml precisely after we've read from disk! - public void Commit(bool registerXmlChange = true) - { - if (_isWriter == false) - throw new InvalidOperationException("Not writing."); - _instance.SetXmlLocked(Xml, registerXmlChange); - _committed = true; - } - - public void Dispose() - { - if (_releaser == null) - return; - if (_isWriter && _auto && _committed == false) - Commit(); - _releaser.Dispose(); - _releaser = null; - } - } - - private static string ChildNodesXPath - { - get { return "./* [@id]"; } - } - - private static string DataNodesXPath - { - get { return "./* [not(@id)]"; } - } - - #endregion - - #region File - - private readonly string _xmlFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); - private DateTime _lastFileRead; // last time the file was read - private DateTime _nextFileCheck; // last time we checked whether the file was changed - - // not used - just try to read the file - //private bool XmlFileExists - //{ - // get - // { - // // check that the file exists and has content (is not empty) - // var fileInfo = new FileInfo(_xmlFileName); - // return fileInfo.Exists && fileInfo.Length > 0; - // } - //} - - private DateTime XmlFileLastWriteTime - { - get - { - var fileInfo = new FileInfo(_xmlFileName); - return fileInfo.Exists ? fileInfo.LastWriteTimeUtc : DateTime.MinValue; - } - } - - // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it - // will NOT try to save once the current app domain is not the main domain anymore - // (no need to test _released) - internal void SaveXmlToFile() - { - LogHelper.Info("Save Xml to file..."); - - try - { - var xml = _xmlContent; // capture (atomic + volatile), immutable anyway - if (xml == null) return; - - // delete existing file, if any - DeleteXmlFile(); - - // ensure cache directory exists - var directoryName = Path.GetDirectoryName(_xmlFileName); - if (directoryName == null) - throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) - Directory.CreateDirectory(directoryName); - - // save - using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) - { - var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); - fs.Write(bytes, 0, bytes.Length); - } - - LogHelper.Info("Saved Xml to file."); - } - catch (Exception e) - { - // if something goes wrong remove the file - DeleteXmlFile(); - - LogHelper.Error("Failed to save Xml to file.", e); - } - } - - // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it - // will NOT try to save once the current app domain is not the main domain anymore - // (no need to test _released) - internal async Task SaveXmlToFileAsync() - { - LogHelper.Info("Save Xml to file..."); - - try - { - var xml = _xmlContent; // capture (atomic + volatile), immutable anyway - if (xml == null) return; - - // delete existing file, if any - DeleteXmlFile(); - - // ensure cache directory exists - var directoryName = Path.GetDirectoryName(_xmlFileName); - if (directoryName == null) - throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) - Directory.CreateDirectory(directoryName); - - // save - using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) - { - var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); - await fs.WriteAsync(bytes, 0, bytes.Length); - } - - LogHelper.Info("Saved Xml to file."); - } - catch (Exception e) - { - // if something goes wrong remove the file - DeleteXmlFile(); - - LogHelper.Error("Failed to save Xml to file.", e); - } - } - - private string SaveXmlToString(XmlDocument xml) - { - // using that one method because we want to have proper indent - // and in addition, writing async is never fully async because - // althouth the writer is async, xml.WriteTo() will not async - - // that one almost works but... "The elements are indented as long as the element - // does not contain mixed content. Once the WriteString or WriteWhitespace method - // is called to write out a mixed element content, the XmlWriter stops indenting. - // The indenting resumes once the mixed content element is closed." - says MSDN - // about XmlWriterSettings.Indent - - // so ImportContent must also make sure of ignoring whitespaces! - - var sb = new StringBuilder(); - using (var xmlWriter = XmlWriter.Create(sb, new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - //OmitXmlDeclaration = true - })) - { - //xmlWriter.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); - xml.WriteTo(xmlWriter); // already contains the xml declaration - } - return sb.ToString(); - } - - private XmlDocument LoadXmlFromFile() - { - // do NOT try to load if we are not the main domain anymore - if (_released) return null; - - LogHelper.Info("Load Xml from file..."); - - try - { - var xml = new XmlDocument(); - using (var fs = new FileStream(_xmlFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - xml.Load(fs); - } - _lastFileRead = DateTime.UtcNow; - LogHelper.Info("Loaded Xml from file."); - return xml; - } - catch (FileNotFoundException) - { - LogHelper.Warn("Failed to load Xml, file does not exist."); - return null; - } - catch (Exception e) - { - LogHelper.Error("Failed to load Xml from file.", e); - DeleteXmlFile(); - return null; - } - } - - private void DeleteXmlFile() - { - if (File.Exists(_xmlFileName) == false) return; - File.SetAttributes(_xmlFileName, FileAttributes.Normal); - File.Delete(_xmlFileName); - } - - private void ReloadXmlFromFileIfChanged() - { - if (SyncFromXmlFile == false) return; - - var now = DateTime.UtcNow; - if (now < _nextFileCheck) return; - - // time to check - _nextFileCheck = now.AddSeconds(1); // check every 1s - if (XmlFileLastWriteTime <= _lastFileRead) return; - - LogHelper.Debug("Xml file change detected, reloading."); - - // time to read - - using (var safeXml = GetSafeXmlWriter(false)) - { - bool registerXmlChange; - LoadXmlLocked(safeXml, out registerXmlChange); // updates _lastFileRead - safeXml.Commit(registerXmlChange); - } - } - - #endregion - - #region Manage change - - //TODO remove as soon as we can break backward compatibility - [Obsolete("Use GetAddOrUpdateXmlNode which returns an updated Xml document.", false)] - public static void AddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) - { - GetAddOrUpdateXmlNode(xml, id, level, parentId, docNode); - } - - // adds or updates a node (docNode) into a cache (xml) - public static XmlDocument GetAddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) - { - // sanity checks - if (id != docNode.AttributeValue("id")) - throw new ArgumentException("Values of id and docNode/@id are different."); - if (parentId != docNode.AttributeValue("parentID")) - throw new ArgumentException("Values of parentId and docNode/@parentID are different."); - - // find the document in the cache - XmlNode currentNode = xml.GetElementById(id.ToInvariantString()); - - // if the document is not there already then it's a new document - // we must make sure that its document type exists in the schema - if (currentNode == null) - { - var xml2 = EnsureSchema(docNode.Name, xml); - if (ReferenceEquals(xml, xml2) == false) - docNode = xml2.ImportNode(docNode, true); - xml = xml2; - } - - // find the parent - XmlNode parentNode = level == 1 - ? xml.DocumentElement - : xml.GetElementById(parentId.ToInvariantString()); - - // no parent = cannot do anything - if (parentNode == null) - return xml; - - // insert/move the node under the parent - if (currentNode == null) - { - // document not there, new node, append - currentNode = docNode; - parentNode.AppendChild(currentNode); - } - else - { - // document found... we could just copy the currentNode children nodes over under - // docNode, then remove currentNode and insert docNode... the code below tries to - // be clever and faster, though only benchmarking could tell whether it's worth the - // pain... - - // first copy current parent ID - so we can compare with target parent - var moving = currentNode.AttributeValue("parentID") != parentId; - - if (docNode.Name == currentNode.Name) - { - // name has not changed, safe to just update the current node - // by transfering values eg copying the attributes, and importing the data elements - TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode); - - // if moving, move the node to the new parent - // else it's already under the right parent - // (but maybe the sort order has been updated) - if (moving) - parentNode.AppendChild(currentNode); // remove then append to parentNode - } - else - { - // name has changed, must use docNode (with new name) - // move children nodes from currentNode to docNode (already has properties) - var children = currentNode.SelectNodes(ChildNodesXPath); - if (children == null) throw new Exception("oops"); - foreach (XmlNode child in children) - docNode.AppendChild(child); // remove then append to docNode - - // and put docNode in the right place - if parent has not changed, then - // just replace, else remove currentNode and insert docNode under the right parent - // (but maybe not at the right position due to sort order) - if (moving) - { - if (currentNode.ParentNode == null) throw new Exception("oops"); - currentNode.ParentNode.RemoveChild(currentNode); - parentNode.AppendChild(docNode); - } - else - { - // replacing might screw the sort order - parentNode.ReplaceChild(docNode, currentNode); - } - - currentNode = docNode; - } - } - - // if the nodes are not ordered, must sort - // (see U4-509 + has to work with ReplaceChild too) - //XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue("sortOrder")); - - // but... - // if we assume that nodes are always correctly sorted - // then we just need to ensure that currentNode is at the right position. - // should be faster that moving all the nodes around. - XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); - return xml; - } - - private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode documentNode, XmlNode publishedNode) - { - // remove all attributes from the published node - if (publishedNode.Attributes == null) throw new Exception("oops"); - publishedNode.Attributes.RemoveAll(); - - // remove all data nodes from the published node - var dataNodes = publishedNode.SelectNodes(DataNodesXPath); - if (dataNodes == null) throw new Exception("oops"); - foreach (XmlNode n in dataNodes) - publishedNode.RemoveChild(n); - - // append all attributes from the document node to the published node - if (documentNode.Attributes == null) throw new Exception("oops"); - foreach (XmlAttribute att in documentNode.Attributes) - ((XmlElement)publishedNode).SetAttribute(att.Name, att.Value); - - // find the first child node, if any - var childNodes = publishedNode.SelectNodes(ChildNodesXPath); - if (childNodes == null) throw new Exception("oops"); - var firstChildNode = childNodes.Count == 0 ? null : childNodes[0]; - - // append all data nodes from the document node to the published node - dataNodes = documentNode.SelectNodes(DataNodesXPath); - if (dataNodes == null) throw new Exception("oops"); - foreach (XmlNode n in dataNodes) - { - if (publishedNode.OwnerDocument == null) throw new Exception("oops"); - var imported = publishedNode.OwnerDocument.ImportNode(n, true); - if (firstChildNode == null) - publishedNode.AppendChild(imported); - else - publishedNode.InsertBefore(imported, firstChildNode); - } - } - - #endregion - - #region Events - - public delegate void DocumentCacheEventHandler(Document sender, DocumentCacheEventArgs e); - - public delegate void RefreshContentEventHandler(Document sender, RefreshContentEventArgs e); - - /// - /// Occurs when [after document cache update]. - /// - public static event DocumentCacheEventHandler AfterUpdateDocumentCache; - - /// - /// Fires after document cache updater. - /// - /// The sender. - /// The instance containing the event data. - protected virtual void FireAfterUpdateDocumentCache(Document sender, DocumentCacheEventArgs e) - { - if (AfterUpdateDocumentCache != null) - { - AfterUpdateDocumentCache(sender, e); - } - } - - public static event DocumentCacheEventHandler AfterClearDocumentCache; - - /// - /// Fires the after document cache unpublish. - /// - /// The sender. - /// The instance containing the event data. - protected virtual void FireAfterClearDocumentCache(Document sender, DocumentCacheEventArgs e) - { - if (AfterClearDocumentCache != null) - { - AfterClearDocumentCache(sender, e); - } - } - - - public class DocumentCacheEventArgs : System.ComponentModel.CancelEventArgs { } - public class RefreshContentEventArgs : System.ComponentModel.CancelEventArgs { } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/helper.cs b/src/Umbraco.Web/umbraco.presentation/helper.cs index 7e845211b3..66dcdef08b 100644 --- a/src/Umbraco.Web/umbraco.presentation/helper.cs +++ b/src/Umbraco.Web/umbraco.presentation/helper.cs @@ -11,6 +11,7 @@ using umbraco.BusinessLogic; using System.Xml; using umbraco.presentation; using Umbraco.Web; +using Umbraco.Web.Macros; using Umbraco.Web.UI.Pages; namespace umbraco @@ -21,146 +22,28 @@ namespace umbraco [Obsolete("This needs to be removed, do not use")] public class helper { - public static bool IsNumeric(string Number) + public static bool IsNumeric(string number) { int result; - return int.TryParse(Number, out result); + return int.TryParse(number, out result); } - public static String FindAttribute(IDictionary attributes, String key) + public static string FindAttribute(IDictionary attributes, string key) { return FindAttribute(null, attributes, key); } - public static String FindAttribute(IDictionary pageElements, IDictionary attributes, String key) + public static string FindAttribute(IDictionary pageElements, IDictionary attributes, string key) { // fix for issue 14862: lowercase for case insensitive matching key = key.ToLower(); - string attributeValue = string.Empty; + var attributeValue = string.Empty; if (attributes[key] != null) attributeValue = attributes[key].ToString(); - attributeValue = parseAttribute(pageElements, attributeValue); + attributeValue = MacroRenderer.ParseAttribute(pageElements, attributeValue); return attributeValue; } - - /// - /// This method will parse the attribute value to look for some special syntax such as - /// [@requestKey] - /// [%sessionKey] - /// [#pageElement] - /// [$recursiveValue] - /// - /// - /// - /// - /// - /// You can even apply fallback's separated by comma's like: - /// - /// [@requestKey],[%sessionKey] - /// - /// - public static string parseAttribute(IDictionary pageElements, string attributeValue) - { - // Check for potential querystring/cookie variables - // SD: not sure why we are checking for len 3 here? - if (attributeValue.Length > 3 && attributeValue.StartsWith("[")) - { - var attributeValueSplit = (attributeValue).Split(','); - - // before proceeding, we don't want to process anything here unless each item starts/ends with a [ ] - // this is because the attribute value could actually just be a json array like [1,2,3] which we don't want to parse - // - // however, the last one can be a literal, must take care of this! - // so here, don't check the last one, which can be just anything - if (attributeValueSplit.Take(attributeValueSplit.Length - 1).All(x => - //must end with [ - x.EndsWith("]") && - //must start with [ and a special char - (x.StartsWith("[@") || x.StartsWith("[%") || x.StartsWith("[#") || x.StartsWith("[$"))) == false) - { - return attributeValue; - } - - foreach (var attributeValueItem in attributeValueSplit) - { - attributeValue = attributeValueItem; - var trimmedValue = attributeValue.Trim(); - - // Check for special variables (always in square-brackets like [name]) - if (trimmedValue.StartsWith("[") && - trimmedValue.EndsWith("]")) - { - attributeValue = trimmedValue; - - // find key name - var keyName = attributeValue.Substring(2, attributeValue.Length - 3); - var keyType = attributeValue.Substring(1, 1); - - switch (keyType) - { - case "@": - attributeValue = HttpContext.Current.Request[keyName]; - break; - case "%": - attributeValue = HttpContext.Current.Session[keyName] != null ? HttpContext.Current.Session[keyName].ToString() : null; - if (string.IsNullOrEmpty(attributeValue)) - attributeValue = HttpContext.Current.Request.GetCookieValue(keyName); - break; - case "#": - if (pageElements[keyName] != null) - attributeValue = pageElements[keyName].ToString(); - else - attributeValue = ""; - break; - case "$": - if (pageElements[keyName] != null && pageElements[keyName].ToString() != string.Empty) - { - attributeValue = pageElements[keyName].ToString(); - } - else - { - // reset attribute value in case no value has been found on parents - attributeValue = String.Empty; - XmlDocument umbracoXML = global::umbraco.content.Instance.XmlContent; - - String[] splitpath = (String[])pageElements["splitpath"]; - for (int i = 0; i < splitpath.Length - 1; i++) - { - XmlNode element = umbracoXML.GetElementById(splitpath[splitpath.Length - i - 1].ToString()); - if (element == null) - continue; - string xpath = "{0}"; - XmlNode currentNode = element.SelectSingleNode(string.Format(xpath, - keyName)); - if (currentNode != null && currentNode.FirstChild != null && - !string.IsNullOrEmpty(currentNode.FirstChild.Value) && - !string.IsNullOrEmpty(currentNode.FirstChild.Value.Trim())) - { - HttpContext.Current.Trace.Write("parameter.recursive", "Item loaded from " + splitpath[splitpath.Length - i - 1]); - attributeValue = currentNode.FirstChild.Value; - break; - } - } - } - break; - } - - if (attributeValue != null) - { - attributeValue = attributeValue.Trim(); - if (attributeValue != string.Empty) - break; - } - else - attributeValue = string.Empty; - } - } - } - - return attributeValue; - } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index fc7abcb744..9096f684c3 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -123,7 +123,11 @@ namespace umbraco { var content = ""; - var umbracoXml = global::umbraco.content.Instance.XmlContent; + var umbracoContext = UmbracoContext.Current; + var cache = umbracoContext.ContentCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; + if (cache == null) + throw new InvalidOperationException("Unsupported IPublishedContentCache, only the Xml one is supported."); + var umbracoXml = cache.GetXml(umbracoContext.InPreviewMode); var splitpath = (String[])elements["splitpath"]; for (int i = 0; i < splitpath.Length - 1; i++) diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 8519a97159..53ddb50c65 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -29,6 +29,8 @@ using umbraco.cms.businesslogic.web; using umbraco.DataLayer; using Umbraco.Core.IO; using Umbraco.Core.Xml; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.XmlPublishedCache; using Language = umbraco.cms.businesslogic.language.Language; using Media = umbraco.cms.businesslogic.media.Media; using Member = umbraco.cms.businesslogic.member.Member; @@ -38,8 +40,8 @@ namespace umbraco { /// /// Function library for umbraco. Includes various helper-methods and methods to - /// save and load data from umbraco. - /// + /// save and load data from umbraco. + /// /// Especially usefull in XSLT where any of these methods can be accesed using the umbraco.library name-space. Example: /// <xsl:value-of select="umbraco.library:NiceUrl(@id)"/> /// @@ -71,7 +73,7 @@ namespace umbraco private page _page; #endregion - + #region Constructors /// @@ -83,7 +85,8 @@ namespace umbraco public library(int id) { - _page = new page(((System.Xml.IHasXmlNode)GetXmlNodeById(id.ToString()).Current).GetNode()); + var content = GetSafeContentCache().GetById(id); + _page = new page(content); } /// @@ -95,85 +98,6 @@ namespace umbraco _page = page; } - #endregion - - #region Publish Helper Methods - - - /// - /// Unpublish a node, by removing it from the runtime xml index. Note, prior to this the Document should be - /// marked unpublished by setting the publish property on the document object to false - /// - /// The Id of the Document to be unpublished - [Obsolete("This method is no longer used, a document's cache will be removed automatically when the document is deleted or unpublished")] - public static void UnPublishSingleNode(int DocumentId) - { - DistributedCache.Instance.RemovePageCache(DocumentId); - } - - /// - /// Publishes a Document by adding it to the runtime xml index. Note, prior to this the Document should be - /// marked published by calling Publish(User u) on the document object. - /// - /// The Id of the Document to be published - [Obsolete("This method is no longer used, a document's cache will be updated automatically when the document is published")] - public static void UpdateDocumentCache(int documentId) - { - DistributedCache.Instance.RefreshPageCache(documentId); - } - - /// - /// Publishes the single node, this method is obsolete - /// - /// The document id. - [Obsolete("Please use: umbraco.library.UpdateDocumentCache")] - public static void PublishSingleNode(int DocumentId) - { - UpdateDocumentCache(DocumentId); - } - - /// - /// Refreshes the xml cache for all nodes - /// - public static void RefreshContent() - { - DistributedCache.Instance.RefreshAllPageCache(); - } - - /// - /// Re-publishes all nodes under a given node - /// - /// The ID of the node and childnodes that should be republished - [Obsolete("Please use: umbraco.library.RefreshContent")] - public static string RePublishNodes(int nodeID) - { - DistributedCache.Instance.RefreshAllPageCache(); - - return string.Empty; - } - - /// - /// Re-publishes all nodes under a given node - /// - /// The ID of the node and childnodes that should be republished - [Obsolete("Please use: umbraco.library.RefreshContent")] - public static void RePublishNodesDotNet(int nodeID) - { - DistributedCache.Instance.RefreshAllPageCache(); - } - - /// - /// Refreshes the runtime xml index. - /// Note: This *doesn't* mark any non-published document objects as published - /// - /// Always use -1 - /// Not used - [Obsolete("Please use: content.Instance.RefreshContentFromDatabaseAsync")] - public static void RePublishNodesDotNet(int nodeID, bool SaveToDisk) - { - DistributedCache.Instance.RefreshAllPageCache(); - } - #endregion #region Xslt Helper functions @@ -201,8 +125,8 @@ namespace umbraco xd.LoadXml(string.Format("Could not convert JSON to XML. Error: {0}", ex)); return xd.CreateNavigator().Select("/error"); } - } - + } + /// /// Returns a string with a friendly url from a node. /// IE.: Instead of having /482 (id) as an url, you can have @@ -212,48 +136,36 @@ namespace umbraco /// String with a friendly url from a node public static string NiceUrl(int nodeID) { - return GetUmbracoHelper().NiceUrl(nodeID); + return GetUmbracoHelper().NiceUrl(nodeID); } /// - /// This method will always add the root node to the path. You should always use NiceUrl, as that is the - /// only one who checks for toplevel node settings in the web.config + /// This method will always add the domain to the path if the hostnames are set up correctly. /// - /// Identifier for the node that should be returned - /// String with a friendly url from a node - [Obsolete] - public static string NiceUrlFullPath(int nodeID) - { - throw new NotImplementedException("It was broken anyway..."); - } - - /// - /// This method will always add the domain to the path if the hostnames are set up correctly. - /// - /// Identifier for the node that should be returned + /// Identifier for the node that should be returned /// String with a friendly url with full domain from a node - public static string NiceUrlWithDomain(int nodeID) + public static string NiceUrlWithDomain(int nodeId) { - return GetUmbracoHelper().NiceUrlWithDomain(nodeID); + return GetUmbracoHelper().NiceUrlWithDomain(nodeId); } /// - /// This method will always add the domain to the path. + /// This method will always add the domain to the path. /// - /// Identifier for the node that should be returned + /// Identifier for the node that should be returned /// Ignores the umbraco hostnames and returns the url prefixed with the requested host (including scheme and port number) /// String with a friendly url with full domain from a node - internal static string NiceUrlWithDomain(int nodeID, bool ignoreUmbracoHostNames) + internal static string NiceUrlWithDomain(int nodeId, bool ignoreUmbracoHostNames) { if (ignoreUmbracoHostNames) - return HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + NiceUrl(nodeID); + return HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + NiceUrl(nodeId); - return NiceUrlWithDomain(nodeID); + return NiceUrlWithDomain(nodeId); } public static string ResolveVirtualPath(string path) { - return Umbraco.Core.IO.IOHelper.ResolveUrl(path); + return IOHelper.ResolveUrl(path); } @@ -263,12 +175,12 @@ namespace umbraco /// getItem(1, nodeName) will return a string with the name of the node with id=1 even though /// nodeName is a property and not an element (data-field). /// - /// Identifier for the node that should be returned + /// Identifier for the node that should be returned /// The element that should be returned /// Returns a string with the data from the given element of a node - public static string GetItem(int nodeID, String alias) + public static string GetItem(int nodeId, string alias) { - var doc = Umbraco.Web.UmbracoContext.Current.ContentCache.GetById(nodeID); + var doc = UmbracoContext.Current.ContentCache.GetById(nodeId); if (doc == null) return string.Empty; @@ -318,11 +230,11 @@ namespace umbraco /// /// Checks with the Assigned domains settings and retuns an array the the Domains matching the node /// - /// Identifier for the node that should be returned + /// Identifier for the node that should be returned /// A Domain array with all the Domains that matches the nodeId - public static Domain[] GetCurrentDomains(int NodeId) + public static Domain[] GetCurrentDomains(int nodeId) { - string[] pathIds = GetItem(NodeId, "path").Split(','); + string[] pathIds = GetItem(nodeId, "path").Split(','); for (int i = pathIds.Length - 1; i > 0; i--) { Domain[] retVal = Domain.GetDomainsById(int.Parse(pathIds[i])); @@ -342,7 +254,7 @@ namespace umbraco /// /// /// - public static string GetItem(String alias) + public static string GetItem(string alias) { try { @@ -359,15 +271,15 @@ namespace umbraco /// /// Returns that name of a generic property /// - /// The Alias of the content type (ie. Document Type, Member Type or Media Type) - /// The Alias of the Generic property (ie. bodyText or umbracoNaviHide) + /// The Alias of the content type (ie. Document Type, Member Type or Media Type) + /// The Alias of the Generic property (ie. bodyText or umbracoNaviHide) /// A string with the name. If nothing matches the alias, an empty string is returned - public static string GetPropertyTypeName(string ContentTypeAlias, string PropertyTypeAlias) + public static string GetPropertyTypeName(string contentTypeAlias, string propertyTypeAlias) { try { - umbraco.cms.businesslogic.ContentType ct = umbraco.cms.businesslogic.ContentType.GetByAlias(ContentTypeAlias); - PropertyType pt = ct.getPropertyType(PropertyTypeAlias); + umbraco.cms.businesslogic.ContentType ct = umbraco.cms.businesslogic.ContentType.GetByAlias(contentTypeAlias); + PropertyType pt = ct.getPropertyType(propertyTypeAlias); return pt.Name; } catch @@ -379,15 +291,15 @@ namespace umbraco /// /// Returns the Member Name from an umbraco member object /// - /// The identifier of the Member + /// The identifier of the Member /// The Member name matching the MemberId, an empty string is member isn't found - public static string GetMemberName(int MemberId) + public static string GetMemberName(int memberId) { - if (MemberId != 0) + if (memberId != 0) { try { - Member m = new Member(MemberId); + Member m = new Member(memberId); return m.Text; } catch @@ -403,31 +315,30 @@ namespace umbraco /// Get a media object as an xml object /// /// The identifier of the media object to be returned - /// If true, children of the media object is returned + /// If true, children of the media object is returned /// An umbraco xml node of the media (same format as a document node) - public static XPathNodeIterator GetMedia(int MediaId, bool Deep) + public static XPathNodeIterator GetMedia(int MediaId, bool deep) { try { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) { var xml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( - string.Format( - "{0}_{1}_{2}", CacheKeys.MediaCacheKey, MediaId, Deep), + $"{CacheKeys.MediaCacheKey}_{MediaId}_{deep}", timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - getCacheItem: () => GetMediaDo(MediaId, Deep)); + getCacheItem: () => GetMediaDo(MediaId, deep).Item1); if (xml != null) - { + { //returning the root element of the Media item fixes the problem return xml.CreateNavigator().Select("/"); } - + } else { - var xml = GetMediaDo(MediaId, Deep); - + var xml = GetMediaDo(MediaId, deep).Item1; + //returning the root element of the Media item fixes the problem return xml.CreateNavigator().Select("/"); } @@ -443,19 +354,19 @@ namespace umbraco return errorXml.CreateNavigator().Select("/"); } - private static XElement GetMediaDo(int mediaId, bool deep) + private static Tuple GetMediaDo(int mediaId, bool deep) { var media = ApplicationContext.Current.Services.MediaService.GetById(mediaId); if (media == null) return null; var serializer = new EntityXmlSerializer(); var serialized = serializer.Serialize( - ApplicationContext.Current.Services.MediaService, + ApplicationContext.Current.Services.MediaService, ApplicationContext.Current.Services.DataTypeService, - ApplicationContext.Current.Services.UserService, + ApplicationContext.Current.Services.UserService, UrlSegmentProviderResolver.Current.Providers, - media, + media, deep); - return serialized; + return Tuple.Create(serialized, media.Path); } /// @@ -518,10 +429,9 @@ namespace umbraco Member m = Member.GetCurrentMember(); if (m != null) { - XmlDocument mXml = new XmlDocument(); - mXml.LoadXml(m.ToXml(mXml, false).OuterXml); - XPathNavigator xp = mXml.CreateNavigator(); - return xp.Select("/node()"); + var n = global::Umbraco.Web.UmbracoContext.Current.Facade.MemberCache.CreateNodeNavigator(m.Id, false); + if (n != null) + return n.Select("."); // vs "/node" vs "/node()" ? } XmlDocument xd = new XmlDocument(); @@ -926,7 +836,7 @@ namespace umbraco /// /// Renders the content of a macro. Uses the normal template umbraco macro markup as input. - /// This only works properly with xslt macros. + /// This only works properly with xslt macros. /// Python and .ascx based macros will not render properly, as viewstate is not included. /// /// The macro markup to be rendered. @@ -936,7 +846,7 @@ namespace umbraco { try { - page p = new page(((IHasXmlNode)GetXmlNodeById(PageId.ToString()).Current).GetNode()); + var p = new page(GetSafeContentCache().GetById(PageId)); template t = new template(p.Template); Control c = t.parseStringBuilder(new StringBuilder(Text), p); @@ -980,18 +890,17 @@ namespace umbraco } else { - - var p = new page(((IHasXmlNode)GetXmlNodeById(PageId.ToString()).Current).GetNode()); + var p = new page(GetSafeContentCache().GetById(PageId)); p.RenderPage(TemplateId); var c = p.PageContentControl; - + using (var sw = new StringWriter()) using(var hw = new HtmlTextWriter(sw)) { c.RenderControl(hw); - return sw.ToString(); + return sw.ToString(); } - + } } @@ -1197,7 +1106,7 @@ namespace umbraco /// Returns the prevalues as a XPathNodeIterator in the format: /// /// [value] - /// + /// /// public static XPathNodeIterator GetPreValues(int DataTypeId) { @@ -1212,7 +1121,7 @@ namespace umbraco n.Attributes.Append(XmlHelper.AddAttribute(xd, "id", dr.id.ToString())); xd.DocumentElement.AppendChild(n); } - + XPathNavigator xp = xd.CreateNavigator(); return xp.Select("/preValues"); } @@ -1298,7 +1207,7 @@ namespace umbraco /// A dictionary items value as a string. public static string GetDictionaryItem(string Key) { - return GetUmbracoHelper().GetDictionaryValue(Key); + return GetUmbracoHelper().GetDictionaryValue(Key); } /// @@ -1309,7 +1218,7 @@ namespace umbraco { try { - var nav = Umbraco.Web.UmbracoContext.Current.ContentCache.GetXPathNavigator(); + var nav = Umbraco.Web.UmbracoContext.Current.ContentCache.CreateNavigator(); nav.MoveToId(HttpContext.Current.Items["pageID"].ToString()); return nav.Select("."); } @@ -1330,28 +1239,40 @@ namespace umbraco /// Returns the node with the specified id as xml in the form of a XPathNodeIterator public static XPathNodeIterator GetXmlNodeById(string id) { - // 4.7.1 UmbracoContext is null if we're running in publishing thread which we need to support - XmlDocument xmlDoc = GetThreadsafeXmlDocument(); + var nav = GetSafeContentCache().CreateNavigator(); - if (xmlDoc.GetElementById(id) != null) + if (nav.MoveToId(id)) + return nav.Select("."); + + var xd = new XmlDocument(); + xd.LoadXml(string.Format("No published item exist with id {0}", id)); + return xd.CreateNavigator().Select("."); + } + + // legacy would access the raw XML from content.Instance ie a static thing + // now that we use a FacadeService, we need to have a "context" to handle a cache. + // UmbracoContext does it for most cases but in some cases we might not have an + // UmbracoContext. For backward compatibility, try to do something here... + internal static PublishedContentCache GetSafeContentCache() + { + PublishedContentCache contentCache; + + if (UmbracoContext.Current != null) { - XPathNavigator xp = xmlDoc.CreateNavigator(); - xp.MoveToId(id); - return xp.Select("."); + contentCache = UmbracoContext.Current.ContentCache as PublishedContentCache; } else { - XmlDocument xd = new XmlDocument(); - xd.LoadXml(string.Format("No published item exist with id {0}", id)); - return xd.CreateNavigator().Select("."); + // FIXME inject! + var caches = FacadeServiceResolver.Current.Service.GetFacade() + ?? FacadeServiceResolver.Current.Service.CreateFacade(null); + contentCache = caches.ContentCache as PublishedContentCache; } - } - //TODO: WTF, why is this here? This won't matter if there's an UmbracoContext or not, it will call the same underlying method! - // only difference is that the UmbracoContext way will check if its in preview mode. - private static XmlDocument GetThreadsafeXmlDocument() - { - return content.Instance.XmlContent; + if (contentCache == null) + throw new InvalidOperationException("Unsupported IPublishedContentCache, only the Xml one is supported."); + + return contentCache; } /// @@ -1361,9 +1282,7 @@ namespace umbraco /// Returns nodes matching the xpath query as a XpathNodeIterator public static XPathNodeIterator GetXmlNodeByXPath(string xpathQuery) { - XPathNavigator xp = GetThreadsafeXmlDocument().CreateNavigator(); - - return xp.Select(xpathQuery); + return GetSafeContentCache().CreateNavigator().Select(xpathQuery); } /// @@ -1372,8 +1291,7 @@ namespace umbraco /// Returns the entire umbraco Xml cache as a XPathNodeIterator public static XPathNodeIterator GetXmlAll() { - XPathNavigator xp = GetThreadsafeXmlDocument().CreateNavigator(); - return xp.Select("/root"); + return GetSafeContentCache().CreateNavigator().Select("/root"); } /// @@ -1464,21 +1382,23 @@ namespace umbraco /// The Xpath query for the node with the specified id as a string public static string QueryForNode(string id) { - string XPathQuery = string.Empty; - if (content.Instance.XmlContent.GetElementById(id) != null) + var xpathQuery = string.Empty; + var preview = UmbracoContext.Current != null && UmbracoContext.Current.InPreviewMode; + var xml = GetSafeContentCache().GetXml(preview); + var elt = xml.GetElementById(id); + + if (elt == null) return xpathQuery; + + var path = elt.Attributes["path"].Value.Split((",").ToCharArray()); + for (var i = 1; i < path.Length; i++) { - string[] path = - content.Instance.XmlContent.GetElementById(id).Attributes["path"].Value.Split((",").ToCharArray()); - for (int i = 1; i < path.Length; i++) - { - if (i > 1) - XPathQuery += "/node [@id = " + path[i] + "]"; - else - XPathQuery += " [@id = " + path[i] + "]"; - } + if (i > 1) + xpathQuery += "/node [@id = " + path[i] + "]"; + else + xpathQuery += " [@id = " + path[i] + "]"; } - return XPathQuery; + return xpathQuery; } /// @@ -1516,7 +1436,7 @@ namespace umbraco { try { - // create the mail message + // create the mail message MailMessage mail = new MailMessage(FromMail.Trim(), ToMail.Trim()); // populate the message @@ -1538,17 +1458,17 @@ namespace umbraco } } - /// + /// /// These random methods are from Eli Robillards blog - kudos for the work :-) /// http://weblogs.asp.net/erobillard/archive/2004/05/06/127374.aspx - /// - /// Get a Random object which is cached between requests. - /// The advantage over creating the object locally is that the .Next - /// call will always return a new value. If creating several times locally - /// with a generated seed (like millisecond ticks), the same number can be - /// returned. - /// - /// A Random object which is cached between calls. + /// + /// Get a Random object which is cached between requests. + /// The advantage over creating the object locally is that the .Next + /// call will always return a new value. If creating several times locally + /// with a generated seed (like millisecond ticks), the same number can be + /// returned. + /// + /// A Random object which is cached between calls. public static Random GetRandom(int seed) { Random r = (Random)HttpContext.Current.Cache.Get("RandomNumber"); @@ -1563,10 +1483,10 @@ namespace umbraco return r; } - /// - /// GetRandom with no parameters. - /// - /// A Random object which is cached between calls. + /// + /// GetRandom with no parameters. + /// + /// A Random object which is cached between calls. public static Random GetRandom() { return GetRandom(0); @@ -1701,7 +1621,7 @@ namespace umbraco } /// - /// URL-encodes a string + /// URL-encodes a string /// /// The string to be encoded /// A URL-encoded string @@ -1711,7 +1631,7 @@ namespace umbraco } /// - /// HTML-encodes a string + /// HTML-encodes a string /// /// The string to be encoded /// A HTML-encoded string @@ -1725,30 +1645,6 @@ namespace umbraco return ApplicationContext.Current.Services.RelationService.GetByParentOrChildId(nodeId).ToArray(); } - [Obsolete("Use DistributedCache.Instance.RemoveMediaCache instead")] - public static void ClearLibraryCacheForMedia(int mediaId) - { - DistributedCache.Instance.RemoveMediaCache(mediaId); - } - - [Obsolete("Use DistributedCache.Instance.RemoveMediaCache instead")] - public static void ClearLibraryCacheForMediaDo(int mediaId) - { - DistributedCache.Instance.RemoveMediaCache(mediaId); - } - - [Obsolete("Use DistributedCache.Instance.RefreshMemberCache instead")] - public static void ClearLibraryCacheForMember(int mediaId) - { - DistributedCache.Instance.RefreshMemberCache(mediaId); - } - - [Obsolete("Use DistributedCache.Instance.RefreshMemberCache instead")] - public static void ClearLibraryCacheForMemberDo(int memberId) - { - DistributedCache.Instance.RefreshMemberCache(memberId); - } - /// /// Gets the related nodes, of the node with the specified Id, as XML. /// @@ -1766,7 +1662,7 @@ namespace umbraco XmlDocument xd = new XmlDocument(); xd.LoadXml(""); - var rels = ApplicationContext.Current.Services.RelationService.GetByParentOrChildId(NodeId); + var rels = ApplicationContext.Current.Services.RelationService.GetByParentOrChildId(NodeId); foreach (var r in rels) { XmlElement n = xd.CreateElement("relation"); @@ -1804,7 +1700,7 @@ namespace umbraco n.AppendChild(x); } } - + xd.DocumentElement.AppendChild(n); } XPathNavigator xp = xd.CreateNavigator(); @@ -1854,7 +1750,7 @@ namespace umbraco } - + #endregion diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs deleted file mode 100644 index 93418bad18..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ /dev/null @@ -1,1678 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using System.Web; -using System.Web.Caching; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Xml; -using System.Xml.XPath; -using System.Xml.Xsl; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Events; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Macros; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Xml.XPath; -using Umbraco.Core.Profiling; -using Umbraco.Web; -using Umbraco.Web.Cache; -using Umbraco.Web.Macros; -using Umbraco.Web.Models; -using Umbraco.Web.Templates; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic.macro; -using umbraco.DataLayer; -using umbraco.presentation.templateControls; -using Umbraco.Web.umbraco.presentation; -using Content = umbraco.cms.businesslogic.Content; -using Macro = umbraco.cms.businesslogic.macro.Macro; -using MacroErrorEventArgs = Umbraco.Core.Events.MacroErrorEventArgs; -using System.Linq; -using Umbraco.Core.Xml; -using File = System.IO.File; -using Member = umbraco.cms.businesslogic.member.Member; - -namespace umbraco -{ - /// - /// Summary description for macro. - /// - public class macro - { - #region private properties - - /// Cache for . - private static Dictionary _predefinedExtensions; - private static XsltSettings _xsltSettings; - private const string LoadUserControlKey = "loadUserControl"; - private readonly StringBuilder _content = new StringBuilder(); - private const string MacrosAddedKey = "macrosAdded"; - public IList Exceptions = new List(); - - static macro() - { - _xsltSettings = SystemUtilities.GetCurrentTrustLevel() > AspNetHostingPermissionLevel.Medium - ? XsltSettings.TrustedXslt - : XsltSettings.Default; - } - - #endregion - - #region public properties - - public bool CacheByPersonalization - { - get { return Model.CacheByMember; } - } - - public bool CacheByPage - { - get { return Model.CacheByPage; } - } - - public bool DontRenderInEditor - { - get { return !Model.RenderInEditor; } - } - - public int RefreshRate - { - get { return Model.CacheDuration; } - } - - public String Alias - { - get { return Model.Alias; } - } - - public String Name - { - get { return Model.Name; } - } - - public String XsltFile - { - get { return Model.Xslt; } - } - - public String ScriptFile - { - get { return Model.ScriptName; } - } - - public String ScriptType - { - get { return Model.TypeName; } - } - - public String ScriptAssembly - { - get { return Model.TypeName; } - } - - public int MacroType - { - get { return (int)Model.MacroType; } - } - - public String MacroContent - { - set { _content.Append(value); } - get { return _content.ToString(); } - } - - #endregion - - #region REFACTOR - - /// - /// Creates a macro object - /// - /// Specify the macro-id which should be loaded (from table macro) - public macro(int id) - { - Macro m = Macro.GetById(id); - Model = new MacroModel(m); - } - - public macro(string alias) - { - Macro m = Macro.GetByAlias(alias); - Model = new MacroModel(m); - } - - public MacroModel Model { get; set; } - - public static macro GetMacro(string alias) - { - return new macro(alias); - } - - public static macro GetMacro(int id) - { - return new macro(id); - } - - #endregion - - - /// - /// Creates an empty macro object. - /// - public macro() - { - Model = new MacroModel(); - } - - - public override string ToString() - { - return Model.Name; - } - - - string GetCacheIdentifier(MacroModel model, Hashtable pageElements, int pageId) - { - var id = new StringBuilder(); - - var alias = string.IsNullOrEmpty(model.ScriptCode) ? model.Alias : Macro.GenerateCacheKeyFromCode(model.ScriptCode); - id.AppendFormat("{0}-", alias); - - if (CacheByPage) - { - id.AppendFormat("{0}-", pageId); - } - - if (CacheByPersonalization) - { - object memberId = 0; - if (HttpContext.Current.User.Identity.IsAuthenticated) - { - var provider = Umbraco.Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - var member = Umbraco.Core.Security.MembershipProviderExtensions.GetCurrentUser(provider); - if (member != null) - { - memberId = member.ProviderUserKey ?? 0; - } - } - id.AppendFormat("m{0}-", memberId); - } - - foreach (var prop in model.Properties) - { - var propValue = prop.Value; - id.AppendFormat("{0}-", propValue.Length <= 255 ? propValue : propValue.Substring(0, 255)); - } - - return id.ToString(); - } - - public Control RenderMacro(Hashtable attributes, Hashtable pageElements, int pageId) - { - // TODO: Parse attributes - UpdateMacroModel(attributes); - return RenderMacro(pageElements, pageId); - } - - /// - /// An event that is raised just before the macro is rendered allowing developers to modify the macro before it executes. - /// - public static event TypedEventHandler MacroRendering; - - /// - /// Raises the MacroRendering event - /// - /// - protected void OnMacroRendering(MacroRenderingEventArgs e) - { - if (MacroRendering != null) - MacroRendering(this, e); - } - - /// - /// Renders the macro - /// - /// - /// - /// - public Control RenderMacro(Hashtable pageElements, int pageId) - { - // Event to allow manipulation of Macro Model - OnMacroRendering(new MacroRenderingEventArgs(pageElements, pageId)); - - var macroInfo = (Model.MacroType == MacroTypes.PartialView && Model.Name.IsNullOrWhiteSpace()) - ? string.Format("Render Inline Macro, Cache: {0})", Model.CacheDuration) - : string.Format("Render Macro: {0}, type: {1}, cache: {2})", Name, Model.MacroType, Model.CacheDuration); - - using (DisposableTimer.DebugDuration(macroInfo)) - { - TraceInfo("renderMacro", macroInfo, excludeProfiling: true); - - if (HttpContext.Current != null) - { - HttpContext.Current.Items[MacrosAddedKey] = HttpContext.Current.GetContextItem(MacrosAddedKey) + 1; - } - - // zb-00037 #29875 : parse attributes here (and before anything else) - foreach (MacroPropertyModel prop in Model.Properties) - prop.Value = helper.parseAttribute(pageElements, prop.Value); - - Model.CacheIdentifier = GetCacheIdentifier(Model, pageElements, pageId); - - string macroHtml; - Control macroControl; - //get the macro from cache if it is there - GetMacroFromCache(out macroHtml, out macroControl); - - // FlorisRobbemont: Empty macroHtml (not null, but "") doesn't mean a re-render is necessary - if (macroHtml == null && macroControl == null) - { - var renderFailed = false; - var macroType = Model.MacroType != MacroTypes.Unknown - ? (int)Model.MacroType - : MacroType; - var textService = ApplicationContext.Current.Services.TextService; - - switch (macroType) - { - case (int)MacroTypes.PartialView: - - //error handler for partial views, is an action because we need to re-use it twice below - Func handleError = e => - { - LogHelper.WarnWithException("Error loading Partial View (file: " + ScriptFile + ")", true, e); - // Invoke any error handlers for this macro - var macroErrorEventArgs = - new MacroErrorEventArgs - { - Name = Model.Name, - Alias = Model.Alias, - ItemKey = Model.ScriptName, - Exception = e, - Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour - }; - - var errorMessage = textService.Localize("errors/macroErrorLoadingPartialView", new[] { ScriptFile }); - return GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); - }; - - using (DisposableTimer.DebugDuration("Executing Partial View: " + Model.TypeName)) - { - TraceInfo("umbracoMacro", "Partial View added (" + Model.TypeName + ")", excludeProfiling: true); - try - { - var result = LoadPartialViewMacro(Model); - macroControl = new LiteralControl(result.Result); - if (result.ResultException != null) - { - renderFailed = true; - Exceptions.Add(result.ResultException); - macroControl = handleError(result.ResultException); - //if it is null, then we are supposed to throw the exception - if (macroControl == null) - { - throw result.ResultException; - } - } - } - catch (Exception e) - { - LogHelper.WarnWithException( - "Error loading partial view macro (View: " + Model.ScriptName + ")", true, e); - - renderFailed = true; - Exceptions.Add(e); - macroControl = handleError(e); - //if it is null, then we are supposed to throw the (original) exception - // see: http://issues.umbraco.org/issue/U4-497 at the end - if (macroControl == null) - { - throw; - } - } - - break; - } - case (int)MacroTypes.UserControl: - - using (DisposableTimer.DebugDuration("Executing UserControl: " + Model.TypeName)) - { - try - { - TraceInfo("umbracoMacro", "Usercontrol added (" + Model.TypeName + ")", excludeProfiling: true); - - // Add tilde for v4 defined macros - if (string.IsNullOrEmpty(Model.TypeName) == false && - Model.TypeName.StartsWith("~") == false) - Model.TypeName = "~/" + Model.TypeName; - - macroControl = LoadUserControl(ScriptType, Model, pageElements); - break; - } - catch (Exception e) - { - renderFailed = true; - Exceptions.Add(e); - LogHelper.WarnWithException("Error loading userControl (" + Model.TypeName + ")", true, e); - - // Invoke any error handlers for this macro - var macroErrorEventArgs = new MacroErrorEventArgs - { - Name = Model.Name, - Alias = Model.Alias, - ItemKey = Model.TypeName, - Exception = e, - Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour - }; - - var errorMessage = textService.Localize("errors/macroErrorLoadingUsercontrol", new[] { Model.TypeName }); - macroControl = GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); - //if it is null, then we are supposed to throw the (original) exception - // see: http://issues.umbraco.org/issue/U4-497 at the end - if (macroControl == null) - { - throw; - } - - break; - } - } - - case (int) MacroTypes.Xslt: - macroControl = LoadMacroXslt(this, Model, pageElements, true); - break; - - case (int)MacroTypes.Unknown: - default: - if (GlobalSettings.DebugMode) - { - macroControl = new LiteralControl("<Macro: " + Name + " (" + ScriptAssembly + "," + ScriptType + ")>"); - } - break; - } - - //add to cache if render is successful - if (renderFailed == false) - { - macroControl = AddMacroResultToCache(macroControl); - } - - } - else if (macroControl == null) - { - macroControl = new LiteralControl(macroHtml); - } - - return macroControl; - } - } - - /// - /// Adds the macro result to cache and returns the control since it might be updated - /// - /// - /// - private Control AddMacroResultToCache(Control macroControl) - { - // Add result to cache if successful (and cache is enabled) - if (UmbracoContext.Current.InPreviewMode == false && Model.CacheDuration > 0) - { - // do not add to cache if there's no member and it should cache by personalization - if (!Model.CacheByMember || (Model.CacheByMember && Member.IsLoggedOn())) - { - if (macroControl != null) - { - string dateAddedCacheKey; - - using (DisposableTimer.DebugDuration("Saving MacroContent To Cache: " + Model.CacheIdentifier)) - { - - // NH: Scripts and XSLT can be generated as strings, but not controls as page events wouldn't be hit (such as Page_Load, etc) - if (CacheMacroAsString(Model)) - { - var outputCacheString = ""; - - using (var sw = new StringWriter()) - { - var hw = new HtmlTextWriter(sw); - macroControl.RenderControl(hw); - - outputCacheString = sw.ToString(); - } - - //insert the cache string result - ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( - CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier, - priority: CacheItemPriority.NotRemovable, - timeout: new TimeSpan(0, 0, Model.CacheDuration), - getCacheItem: () => outputCacheString); - - dateAddedCacheKey = CacheKeys.MacroHtmlDateAddedCacheKey + Model.CacheIdentifier; - - // zb-00003 #29470 : replace by text if not already text - // otherwise it is rendered twice - if (!(macroControl is LiteralControl)) - macroControl = new LiteralControl(outputCacheString); - - TraceInfo("renderMacro", - string.Format("Macro Content saved to cache '{0}'.", Model.CacheIdentifier)); - } - else - { - //insert the cache control result - ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( - CacheKeys.MacroControlCacheKey + Model.CacheIdentifier, - priority: CacheItemPriority.NotRemovable, - timeout: new TimeSpan(0, 0, Model.CacheDuration), - getCacheItem: () => new MacroCacheContent(macroControl, macroControl.ID)); - - dateAddedCacheKey = CacheKeys.MacroControlDateAddedCacheKey + Model.CacheIdentifier; - - TraceInfo("renderMacro", - string.Format("Macro Control saved to cache '{0}'.", Model.CacheIdentifier)); - } - - //insert the date inserted (so we can check file modification date) - ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( - dateAddedCacheKey, - priority: CacheItemPriority.NotRemovable, - timeout: new TimeSpan(0, 0, Model.CacheDuration), - getCacheItem: () => DateTime.Now); - - } - - } - } - } - - return macroControl; - } - - /// - /// Returns the cached version of this macro either as a string or as a Control - /// - /// - /// - /// - /// - /// Depending on the type of macro, this will return the result as a string or as a control. This also - /// checks to see if preview mode is activated, if it is then we don't return anything from cache. - /// - private void GetMacroFromCache(out string macroHtml, out Control macroControl) - { - macroHtml = null; - macroControl = null; - - if (UmbracoContext.Current.InPreviewMode == false && Model.CacheDuration > 0) - { - var macroFile = GetMacroFile(Model); - var fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(macroFile)); - - if (CacheMacroAsString(Model)) - { - macroHtml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( - CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier); - - // FlorisRobbemont: - // An empty string means: macroHtml has been cached before, but didn't had any output (Macro doesn't need to be rendered again) - // An empty reference (null) means: macroHtml has NOT been cached before - if (macroHtml != null) - { - if (MacroNeedsToBeClearedFromCache(Model, CacheKeys.MacroHtmlDateAddedCacheKey + Model.CacheIdentifier, fileInfo)) - { - macroHtml = null; - TraceInfo("renderMacro", - string.Format("Macro removed from cache due to file change '{0}'.", - Model.CacheIdentifier)); - } - else - { - TraceInfo("renderMacro", - string.Format("Macro Content loaded from cache '{0}'.", - Model.CacheIdentifier)); - } - - } - } - else - { - var cacheContent = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( - CacheKeys.MacroControlCacheKey + Model.CacheIdentifier); - - if (cacheContent != null) - { - macroControl = cacheContent.Content; - macroControl.ID = cacheContent.ID; - - if (MacroNeedsToBeClearedFromCache(Model, CacheKeys.MacroControlDateAddedCacheKey + Model.CacheIdentifier, fileInfo)) - { - TraceInfo("renderMacro", - string.Format("Macro removed from cache due to file change '{0}'.", - Model.CacheIdentifier)); - macroControl = null; - } - else - { - TraceInfo("renderMacro", - string.Format("Macro Control loaded from cache '{0}'.", - Model.CacheIdentifier)); - } - - } - } - } - } - - /// - /// Raises the error event and based on the error behavior either return a control to display or throw the exception - /// - /// - /// - /// - private Control GetControlForErrorBehavior(string msg, MacroErrorEventArgs args) - { - OnError(args); - - switch (args.Behaviour) - { - case MacroErrorBehaviour.Inline: - return new LiteralControl(msg); - case MacroErrorBehaviour.Silent: - return new LiteralControl(""); - case MacroErrorBehaviour.Throw: - default: - return null; - } - } - - /// - /// check that the file has not recently changed - /// - /// - /// - /// - /// - /// - /// The only reason this is necessary is because a developer might update a file associated with the - /// macro, we need to ensure if that is the case that the cache not be used and it is refreshed. - /// - internal static bool MacroNeedsToBeClearedFromCache(MacroModel model, string dateAddedKey, FileInfo macroFile) - { - if (MacroIsFileBased(model)) - { - var cacheResult = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(dateAddedKey); - - if (cacheResult != null) - { - var dateMacroAdded = cacheResult; - - if (macroFile.LastWriteTime.CompareTo(dateMacroAdded) == 1) - { - TraceInfo("renderMacro", string.Format("Macro needs to be removed from cache due to file change '{0}'.", model.CacheIdentifier)); - return true; - } - } - } - - return false; - } - - internal static string GetMacroFile(MacroModel model) - { - switch (model.MacroType) - { - case MacroTypes.Xslt: - return string.Concat("~/xslt/", model.Xslt); - case MacroTypes.Script: - return string.Concat("~/macroScripts/", model.ScriptName); - case MacroTypes.PartialView: - return model.ScriptName; //partial views are saved with the full virtual path - case MacroTypes.UserControl: - return model.TypeName; //user controls saved with the full virtual path - case MacroTypes.Unknown: - default: - return "/" + model.TypeName; - } - } - - internal static bool MacroIsFileBased(MacroModel model) - { - return model.MacroType != MacroTypes.Unknown; - } - - /// - /// Determine if macro can be cached as string - /// - /// - /// - /// - /// Scripts and XSLT can be generated as strings, but not controls as page events wouldn't be hit (such as Page_Load, etc) - /// - internal static bool CacheMacroAsString(MacroModel model) - { - switch (model.MacroType) - { - case MacroTypes.Xslt: - case MacroTypes.PartialView: - return true; - case MacroTypes.UserControl: - case MacroTypes.Unknown: - default: - return false; - } - } - - public static XslCompiledTransform GetXslt(string XsltFile) - { - //TODO: SD: Do we really need to cache this?? - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.MacroXsltCacheKey + XsltFile, - CacheItemPriority.Default, - new CacheDependency(IOHelper.MapPath(SystemDirectories.Xslt + "/" + XsltFile)), - () => - { - using (var xslReader = new XmlTextReader(IOHelper.MapPath(SystemDirectories.Xslt.EnsureEndsWith('/') + XsltFile))) - { - return CreateXsltTransform(xslReader, GlobalSettings.DebugMode); - } - }); - } - - public void UpdateMacroModel(Hashtable attributes) - { - foreach (MacroPropertyModel mp in Model.Properties) - { - if (attributes.ContainsKey(mp.Key.ToLowerInvariant())) - { - var item = attributes[mp.Key.ToLowerInvariant()]; - - mp.Value = item == null ? string.Empty : item.ToString(); - } - else - { - mp.Value = string.Empty; - } - } - } - - public void GenerateMacroModelPropertiesFromAttributes(Hashtable attributes) - { - foreach (string key in attributes.Keys) - { - Model.Properties.Add(new MacroPropertyModel(key, attributes[key].ToString())); - } - } - - - public static XslCompiledTransform CreateXsltTransform(XmlTextReader xslReader, bool debugMode) - { - var macroXslt = new XslCompiledTransform(debugMode); - var xslResolver = new XmlUrlResolver - { - Credentials = CredentialCache.DefaultCredentials - }; - - xslReader.EntityHandling = EntityHandling.ExpandEntities; - - try - { - macroXslt.Load(xslReader, _xsltSettings, xslResolver); - } - finally - { - xslReader.Close(); - } - - return macroXslt; - } - - #region LoadMacroXslt - - // gets the control for the macro, using GetXsltTransform methods for execution - // will pick XmlDocument or Navigator mode depending on the capabilities of the published caches - internal Control LoadMacroXslt(macro macro, MacroModel model, Hashtable pageElements, bool throwError) - { - if (XsltFile.Trim() == string.Empty) - { - TraceWarn("macro", "Xslt is empty"); - return new LiteralControl(string.Empty); - } - - using (DisposableTimer.DebugDuration("Executing XSLT: " + XsltFile)) - { - XmlDocument macroXml = null; - MacroNavigator macroNavigator = null; - NavigableNavigator contentNavigator = null; - - var canNavigate = - UmbracoContext.Current.ContentCache.XPathNavigatorIsNavigable && - UmbracoContext.Current.MediaCache.XPathNavigatorIsNavigable; - - if (!canNavigate) - { - // get master xml document - var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; - if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); - XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); - macroXml = new XmlDocument(); - macroXml.LoadXml(""); - foreach (var prop in macro.Model.Properties) - { - AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); - } - } - else - { - var parameters = new List(); - contentNavigator = UmbracoContext.Current.ContentCache.GetXPathNavigator() as NavigableNavigator; - var mediaNavigator = UmbracoContext.Current.MediaCache.GetXPathNavigator() as NavigableNavigator; - foreach (var prop in macro.Model.Properties) - { - AddMacroParameter(parameters, contentNavigator, mediaNavigator, prop.Key, prop.Type, prop.Value); - } - macroNavigator = new MacroNavigator(parameters); - } - - if (HttpContext.Current.Request.QueryString["umbDebug"] != null && GlobalSettings.DebugMode) - { - var outerXml = macroXml == null ? macroNavigator.OuterXml : macroXml.OuterXml; - return - new LiteralControl("
Debug from " + - macro.Name + - "

" + HttpContext.Current.Server.HtmlEncode(outerXml) + - "

"); - } - - var textService = ApplicationContext.Current.Services.TextService; - try - { - var xsltFile = GetXslt(XsltFile); - - using (DisposableTimer.DebugDuration("Performing transformation")) - { - try - { - var transformed = canNavigate - ? GetXsltTransformResult(macroNavigator, contentNavigator, xsltFile) // better? - : GetXsltTransformResult(macroXml, xsltFile); // document - var result = CreateControlsFromText(transformed); - - return result; - } - catch (Exception e) - { - Exceptions.Add(e); - LogHelper.WarnWithException("Error parsing XSLT file", e); - - var macroErrorEventArgs = new MacroErrorEventArgs { Name = Model.Name, Alias = Model.Alias, ItemKey = Model.Xslt, Exception = e, Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - - var errorMessage = textService.Localize("errors/macroErrorParsingXSLTFile", new[] { XsltFile }); - var macroControl = GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); - //if it is null, then we are supposed to throw the (original) exception - // see: http://issues.umbraco.org/issue/U4-497 at the end - if (macroControl == null && throwError) - { - throw; - } - return macroControl; - } - } - } - catch (Exception e) - { - Exceptions.Add(e); - LogHelper.WarnWithException("Error loading XSLT " + Model.Xslt, true, e); - - // Invoke any error handlers for this macro - var macroErrorEventArgs = new MacroErrorEventArgs { Name = Model.Name, Alias = Model.Alias, ItemKey = Model.Xslt, Exception = e, Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - var errorMessage = textService.Localize("errors/macroErrorReadingXSLTFile", new[] { XsltFile }); - var macroControl = GetControlForErrorBehavior(errorMessage + XsltFile, macroErrorEventArgs); - //if it is null, then we are supposed to throw the (original) exception - // see: http://issues.umbraco.org/issue/U4-497 at the end - if (macroControl == null && throwError) - { - throw; - } - return macroControl; - } - } - } - - // gets the control for the macro, using GetXsltTransform methods for execution - public Control loadMacroXSLT(macro macro, MacroModel model, Hashtable pageElements) - { - return LoadMacroXslt(macro, model, pageElements, false); - } - - #endregion - - /// - /// Parses the text for umbraco Item controls that need to be rendered. - /// - /// The text to parse. - /// A control containing the parsed text. - protected Control CreateControlsFromText(string text) - { - // the beginning and end tags - const string tagStart = "[[[[umbraco:Item"; - const string tagEnd = "]]]]"; - - // container that will hold parsed controls - var container = new PlaceHolder(); - - // loop through all text - int textPos = 0; - while (textPos < text.Length) - { - // try to find an item tag, carefully staying inside the string bounds (- 1) - int tagStartPos = text.IndexOf(tagStart, textPos); - int tagEndPos = tagStartPos < 0 ? -1 : text.IndexOf(tagEnd, tagStartPos + tagStart.Length - 1); - - // item tag found? - if (tagStartPos >= 0 && tagEndPos >= 0) - { - // add the preceding text as a literal control - if (tagStartPos > textPos) - container.Controls.Add(new LiteralControl(text.Substring(textPos, tagStartPos - textPos))); - - // extract the tag and parse it - string tag = text.Substring(tagStartPos, (tagEndPos + tagEnd.Length) - tagStartPos); - Hashtable attributes = new Hashtable(XmlHelper.GetAttributesFromElement(tag)); - - // create item with the parameters specified in the tag - var item = new Item(); - item.NodeId = helper.FindAttribute(attributes, "nodeid"); - item.Field = helper.FindAttribute(attributes, "field"); - item.Xslt = helper.FindAttribute(attributes, "xslt"); - item.XsltDisableEscaping = helper.FindAttribute(attributes, "xsltdisableescaping") == "true"; - container.Controls.Add(item); - - // advance past the end of the tag - textPos = tagEndPos + tagEnd.Length; - } - else - { - // no more tags found, just add the remaning text - container.Controls.Add(new LiteralControl(text.Substring(textPos))); - textPos = text.Length; - } - } - return container; - } - - #region GetXsltTransform - - // gets the result of the xslt transform with no parameters - XmlDocument mode - public static string GetXsltTransformResult(XmlDocument macroXml, XslCompiledTransform xslt) - { - return GetXsltTransformResult(macroXml, xslt, null); - } - - // gets the result of the xslt transform - XmlDocument mode - public static string GetXsltTransformResult(XmlDocument macroXml, XslCompiledTransform xslt, Dictionary parameters) - { - TextWriter tw = new StringWriter(); - - XsltArgumentList xslArgs; - - using (DisposableTimer.DebugDuration("Adding XSLT Extensions")) - { - xslArgs = AddXsltExtensions(); - var lib = new library(); - xslArgs.AddExtensionObject("urn:umbraco.library", lib); - } - - // Add parameters - if (parameters == null || !parameters.ContainsKey("currentPage")) - { - xslArgs.AddParam("currentPage", string.Empty, library.GetXmlNodeCurrent()); - } - if (parameters != null) - { - foreach (var parameter in parameters) - xslArgs.AddParam(parameter.Key, string.Empty, parameter.Value); - } - - // Do transformation - using (DisposableTimer.DebugDuration("Executing XSLT transform")) - { - xslt.Transform(macroXml.CreateNavigator(), xslArgs, tw); - } - return TemplateUtilities.ResolveUrlsFromTextString(tw.ToString()); - } - - // gets the result of the xslt transform with no parameters - Navigator mode - public static string GetXsltTransformResult(XPathNavigator macroNavigator, XPathNavigator contentNavigator, - XslCompiledTransform xslt) - { - return GetXsltTransformResult(macroNavigator, contentNavigator, xslt, null); - } - - // gets the result of the xslt transform - Navigator mode - public static string GetXsltTransformResult(XPathNavigator macroNavigator, XPathNavigator contentNavigator, - XslCompiledTransform xslt, Dictionary parameters) - { - TextWriter tw = new StringWriter(); - - XsltArgumentList xslArgs; - using (DisposableTimer.DebugDuration("Adding XSLT Extensions")) - { - xslArgs = AddXsltExtensions(); - var lib = new library(); - xslArgs.AddExtensionObject("urn:umbraco.library", lib); - } - - // Add parameters - if (parameters == null || !parameters.ContainsKey("currentPage")) - { - var current = contentNavigator.Clone().Select("//* [@id=" + HttpContext.Current.Items["pageID"] + "]"); - xslArgs.AddParam("currentPage", string.Empty, current); - } - if (parameters != null) - { - foreach (var parameter in parameters) - xslArgs.AddParam(parameter.Key, string.Empty, parameter.Value); - } - - // Do transformation - using (DisposableTimer.DebugDuration("Executing XSLT transform")) - { - xslt.Transform(macroNavigator, xslArgs, tw); - } - return TemplateUtilities.ResolveUrlsFromTextString(tw.ToString()); - } - - #endregion - - public static XsltArgumentList AddXsltExtensions() - { - return AddMacroXsltExtensions(); - } - - /// - /// Gets a collection of all XSLT extensions for macros, including predefined extensions. - /// - /// A dictionary of name/extension instance pairs. - public static Dictionary GetXsltExtensions() - { - return XsltExtensionsResolver.Current.XsltExtensions - .ToDictionary(x => x.Namespace, x => x.ExtensionObject); - } - - /// - /// Returns an XSLT argument list with all XSLT extensions added, - /// both predefined and configured ones. - /// - /// A new XSLT argument list. - public static XsltArgumentList AddMacroXsltExtensions() - { - var xslArgs = new XsltArgumentList(); - - foreach (var extension in GetXsltExtensions()) - { - string extensionNamespace = "urn:" + extension.Key; - xslArgs.AddExtensionObject(extensionNamespace, extension.Value); - TraceInfo("umbracoXsltExtension", - String.Format("Extension added: {0}, {1}", - extensionNamespace, extension.Value.GetType().Name)); - } - - return xslArgs; - } - - #region LoadMacroXslt (2) - - // add elements to the root node, corresponding to parameters - private void AddMacroXmlNode(XmlDocument umbracoXml, XmlDocument macroXml, - string macroPropertyAlias, string macroPropertyType, string macroPropertyValue) - { - XmlNode macroXmlNode = macroXml.CreateNode(XmlNodeType.Element, macroPropertyAlias, string.Empty); - var x = new XmlDocument(); - - // if no value is passed, then use the current "pageID" as value - var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; - - TraceInfo("umbracoMacro", - "Xslt node adding search start (" + macroPropertyAlias + ",'" + - macroPropertyValue + "')"); - - //TODO: WE need to fix this so that we give control of this stuff over to the actual parameter editors! - - switch (macroPropertyType) - { - case "contentTree": - var nodeId = macroXml.CreateAttribute("nodeID"); - nodeId.Value = contentId; - macroXmlNode.Attributes.SetNamedItem(nodeId); - - // Get subs - try - { - macroXmlNode.AppendChild(macroXml.ImportNode(umbracoXml.GetElementById(contentId), true)); - } - catch - { } - break; - - case "contentCurrent": - var importNode = macroPropertyValue == string.Empty - ? umbracoXml.GetElementById(contentId) - : umbracoXml.GetElementById(macroPropertyValue); - - var currentNode = macroXml.ImportNode(importNode, true); - - // remove all sub content nodes - foreach (XmlNode n in currentNode.SelectNodes("node|*[@isDoc]")) - currentNode.RemoveChild(n); - - macroXmlNode.AppendChild(currentNode); - - break; - - case "contentAll": - macroXmlNode.AppendChild(macroXml.ImportNode(umbracoXml.DocumentElement, true)); - break; - - case "contentRandom": - XmlNode source = umbracoXml.GetElementById(contentId); - if (source != null) - { - var sourceList = source.SelectNodes("node|*[@isDoc]"); - if (sourceList.Count > 0) - { - int rndNumber; - var r = library.GetRandom(); - lock (r) - { - rndNumber = r.Next(sourceList.Count); - } - var node = macroXml.ImportNode(sourceList[rndNumber], true); - // remove all sub content nodes - foreach (XmlNode n in node.SelectNodes("node|*[@isDoc]")) - node.RemoveChild(n); - - macroXmlNode.AppendChild(node); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't have children!"); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't exists!"); - break; - - case "mediaCurrent": - if (string.IsNullOrEmpty(macroPropertyValue) == false) - { - var c = new Content(int.Parse(macroPropertyValue)); - macroXmlNode.AppendChild(macroXml.ImportNode(c.ToXml(umbraco.content.Instance.XmlContent, false), true)); - } - break; - - default: - macroXmlNode.InnerText = HttpContext.Current.Server.HtmlDecode(macroPropertyValue); - break; - } - macroXml.FirstChild.AppendChild(macroXmlNode); - } - - // add parameters to the macro parameters collection - private void AddMacroParameter(ICollection parameters, - NavigableNavigator contentNavigator, NavigableNavigator mediaNavigator, - string macroPropertyAlias, string macroPropertyType, string macroPropertyValue) - { - // if no value is passed, then use the current "pageID" as value - var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; - - TraceInfo("umbracoMacro", - "Xslt node adding search start (" + macroPropertyAlias + ",'" + - macroPropertyValue + "')"); - - // beware! do not use the raw content- or media- navigators, but clones !! - - switch (macroPropertyType) - { - case "contentTree": - parameters.Add(new MacroNavigator.MacroParameter( - macroPropertyAlias, - contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty - attributes: new Dictionary { { "nodeID", contentId } })); - - break; - - case "contentPicker": - parameters.Add(new MacroNavigator.MacroParameter( - macroPropertyAlias, - contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty - 0)); - break; - - case "contentSubs": - parameters.Add(new MacroNavigator.MacroParameter( - macroPropertyAlias, - contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty - 1)); - break; - - case "contentAll": - parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, contentNavigator.Clone())); - break; - - case "contentRandom": - var nav = contentNavigator.Clone(); - if (nav.MoveToId(contentId)) - { - var descendantIterator = nav.Select("./* [@isDoc]"); - if (descendantIterator.MoveNext()) - { - // not empty - and won't change - var descendantCount = descendantIterator.Count; - - int index; - var r = library.GetRandom(); - lock (r) - { - index = r.Next(descendantCount); - } - - while (index > 0 && descendantIterator.MoveNext()) - index--; - - var node = descendantIterator.Current.UnderlyingObject as INavigableContent; - if (node != null) - { - nav = contentNavigator.CloneWithNewRoot(node.Id.ToString(CultureInfo.InvariantCulture)); - parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); - } - else - throw new InvalidOperationException("Iterator contains non-INavigableContent elements."); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't have children!"); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't exists!"); - break; - - case "mediaCurrent": - parameters.Add(new MacroNavigator.MacroParameter( - macroPropertyAlias, - mediaNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty - 0)); - break; - - default: - parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, HttpContext.Current.Server.HtmlDecode(macroPropertyValue))); - break; - } - } - - #endregion - - /// - /// Renders a Partial View Macro - /// - /// - /// - internal PartialViewMacroResult LoadPartialViewMacro(MacroModel macro) - { - var retVal = new PartialViewMacroResult(); - var engine = new PartialViewMacroEngine(); - - var ret = engine.Execute(macro, UmbracoContext.Current.PublishedContentRequest.PublishedContent); - - retVal.Result = ret; - return retVal; - } - - /// - /// Loads a custom or webcontrol using reflection into the macro object - /// - /// The assembly to load from - /// Name of the control - /// - public Control LoadControl(string fileName, string controlName, MacroModel model) - { - return LoadControl(fileName, controlName, model, null); - } - - /// - /// Loads a custom or webcontrol using reflection into the macro object - /// - /// The assembly to load from - /// Name of the control - /// - public Control LoadControl(string fileName, string controlName, MacroModel model, Hashtable pageElements) - { - Type type; - Assembly asm; - try - { - string currentAss = IOHelper.MapPath(string.Format("{0}/{1}.dll", SystemDirectories.Bin, fileName)); - - if (!File.Exists(currentAss)) - return new LiteralControl("Unable to load user control because is does not exist: " + fileName); - asm = Assembly.LoadFrom(currentAss); - - TraceInfo("umbracoMacro", "Assembly file " + currentAss + " LOADED!!"); - } - catch - { - throw new ArgumentException(string.Format("ASSEMBLY NOT LOADED PATH: {0} NOT FOUND!!", - IOHelper.MapPath(SystemDirectories.Bin + "/" + fileName + - ".dll"))); - } - - TraceInfo("umbracoMacro", string.Format("Assembly Loaded from ({0}.dll)", fileName)); - type = asm.GetType(controlName); - if (type == null) - return new LiteralControl(string.Format("Unable to get type {0} from assembly {1}", - controlName, asm.FullName)); - - var control = Activator.CreateInstance(type) as Control; - if (control == null) - return new LiteralControl(string.Format("Unable to create control {0} from assembly {1}", - controlName, asm.FullName)); - - - // Properties - UpdateControlProperties(control, model); - return control; - } - - //TODO: SD : We *really* need to get macro's rendering properly with a real macro engine - // and move logic like this (after it is completely overhauled) to a UserControlMacroEngine. - internal static void UpdateControlProperties(Control control, MacroModel model) - { - var type = control.GetType(); - - foreach (var mp in model.Properties) - { - var prop = type.GetProperty(mp.Key); - if (prop == null) - { - TraceWarn("macro", string.Format("control property '{0}' doesn't exist or aren't accessible (public)", mp.Key)); - continue; - } - - var tryConvert = mp.Value.TryConvertTo(prop.PropertyType); - if (tryConvert.Success) - { - try - { - prop.SetValue(control, tryConvert.Result, null); - } - catch (Exception ex) - { - LogHelper.WarnWithException(string.Format("Error adding property '{0}' with value '{1}'", mp.Key, mp.Value), ex); - if (GlobalSettings.DebugMode) - { - TraceWarn("macro.loadControlProperties", string.Format("Error adding property '{0}' with value '{1}'", mp.Key, mp.Value), ex); - } - } - - if (GlobalSettings.DebugMode) - { - TraceInfo("macro.UpdateControlProperties", string.Format("Property added '{0}' with value '{1}'", mp.Key, mp.Value)); - } - } - else - { - LogHelper.Warn(string.Format("Error adding property '{0}' with value '{1}'", mp.Key, mp.Value)); - if (GlobalSettings.DebugMode) - { - TraceWarn("macro.loadControlProperties", string.Format("Error adding property '{0}' with value '{1}'", mp.Key, mp.Value)); - } - } - } - } - - /// - /// Loads an usercontrol using reflection into the macro object - /// - /// Filename of the usercontrol - ie. ~wulff.ascx - /// - /// The page elements. - /// - public Control LoadUserControl(string fileName, MacroModel model, Hashtable pageElements) - { - Mandate.ParameterNotNullOrEmpty(fileName, "fileName"); - Mandate.ParameterNotNull(model, "model"); - - try - { - string userControlPath = IOHelper.FindFile(fileName); - - if (!File.Exists(IOHelper.MapPath(userControlPath))) - throw new UmbracoException(string.Format("UserControl {0} does not exist.", fileName)); - - var oControl = (UserControl)new UserControl().LoadControl(userControlPath); - - int slashIndex = fileName.LastIndexOf("/") + 1; - if (slashIndex < 0) - slashIndex = 0; - - if (!String.IsNullOrEmpty(model.MacroControlIdentifier)) - oControl.ID = model.MacroControlIdentifier; - else - oControl.ID = - string.Format("{0}_{1}", fileName.Substring(slashIndex, fileName.IndexOf(".ascx") - slashIndex), - HttpContext.Current.GetContextItem(MacrosAddedKey)); - - TraceInfo(LoadUserControlKey, string.Format("Usercontrol added with id '{0}'", oControl.ID)); - - UpdateControlProperties(oControl, model); - return oControl; - } - catch (Exception e) - { - LogHelper.WarnWithException(string.Format("Error creating usercontrol ({0})", fileName), true, e); - throw; - } - } - - - private static void TraceInfo(string category, string message, bool excludeProfiling = false) - { - if (HttpContext.Current != null) - HttpContext.Current.Trace.Write(category, message); - - //Trace out to profiling... doesn't actually profile, just for informational output. - if (excludeProfiling == false) - { - using (ApplicationContext.Current.ProfilingLogger.DebugDuration(string.Format("{0}", message))) - { - } - } - } - - private static void TraceWarn(string category, string message, bool excludeProfiling = false) - { - if (HttpContext.Current != null) - HttpContext.Current.Trace.Warn(category, message); - - //Trace out to profiling... doesn't actually profile, just for informational output. - if (excludeProfiling == false) - { - using (ApplicationContext.Current.ProfilingLogger.TraceDuration(string.Format("Warning: {0}", message))) - { - } - } - } - - private static void TraceWarn(string category, string message, Exception ex, bool excludeProfiling = false) - { - if (HttpContext.Current != null) - HttpContext.Current.Trace.Warn(category, message, ex); - - //Trace out to profiling... doesn't actually profile, just for informational output. - if (excludeProfiling == false) - { - using (ApplicationContext.Current.ProfilingLogger.TraceDuration(string.Format("{0}, Error: {1}", message, ex))) - { - } - } - } - - public static string RenderMacroStartTag(Hashtable attributes, int pageId, Guid versionId) - { - string div = "
"; - - return div; - } - - private static string EncodeMacroAttribute(string attributeContents) - { - // Replace linebreaks - attributeContents = attributeContents.Replace("\n", "\\n").Replace("\r", "\\r"); - - // Replace quotes - attributeContents = - attributeContents.Replace("\"", """); - - // Replace tag start/ends - attributeContents = - attributeContents.Replace("<", "<").Replace(">", ">"); - - - return attributeContents; - } - - public static string RenderMacroEndTag() - { - return "
"; - } - - public static string GetRenderedMacro(int macroId, page umbPage, Hashtable attributes, int pageId) - { - macro m = GetMacro(macroId); - Control c = m.RenderMacro(attributes, umbPage.Elements, pageId); - TextWriter writer = new StringWriter(); - var ht = new HtmlTextWriter(writer); - c.RenderControl(ht); - string result = writer.ToString(); - - // remove hrefs - string pattern = "href=\"([^\"]*)\""; - MatchCollection hrefs = - Regex.Matches(result, pattern, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - foreach (Match href in hrefs) - result = result.Replace(href.Value, "href=\"javascript:void(0)\""); - - return result; - } - - public static string MacroContentByHttp(int pageId, Guid pageVersion, Hashtable attributes) - { - - if (SystemUtilities.GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) - { - return "Cannot render macro content in the rich text editor when the application is running in a Partial Trust environment"; - } - - string tempAlias = (attributes["macroalias"] != null) - ? attributes["macroalias"].ToString() - : attributes["macroAlias"].ToString(); - macro currentMacro = GetMacro(tempAlias); - if (!currentMacro.DontRenderInEditor) - { - string querystring = "umbPageId=" + pageId + "&umbVersionId=" + pageVersion; - IDictionaryEnumerator ide = attributes.GetEnumerator(); - while (ide.MoveNext()) - querystring += "&umb_" + ide.Key + "=" + HttpContext.Current.Server.UrlEncode((ide.Value ?? string.Empty).ToString()); - - // Create a new 'HttpWebRequest' Object to the mentioned URL. - string retVal = string.Empty; - string protocol = GlobalSettings.UseSSL ? "https" : "http"; - string url = string.Format("{0}://{1}:{2}{3}/macroResultWrapper.aspx?{4}", protocol, - HttpContext.Current.Request.ServerVariables["SERVER_NAME"], - HttpContext.Current.Request.ServerVariables["SERVER_PORT"], - IOHelper.ResolveUrl(SystemDirectories.Umbraco), querystring); - - var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); - - // allows for validation of SSL conversations (to bypass SSL errors in debug mode!) - ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate; - - // propagate the user's context - // zb-00004 #29956 : refactor cookies names & handling - - //TODO: This is the worst thing ever. This will also not work if people decide to put their own - // custom auth system in place. - - HttpCookie inCookie = HttpContext.Current.Request.Cookies[UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName]; - if (inCookie == null) throw new NullReferenceException("No auth cookie found"); - var cookie = new Cookie(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, - inCookie.Value, - inCookie.Path, - HttpContext.Current.Request.ServerVariables["SERVER_NAME"]); - myHttpWebRequest.CookieContainer = new CookieContainer(); - myHttpWebRequest.CookieContainer.Add(cookie); - - // Assign the response object of 'HttpWebRequest' to a 'HttpWebResponse' variable. - HttpWebResponse myHttpWebResponse = null; - try - { - myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse(); - if (myHttpWebResponse.StatusCode == HttpStatusCode.OK) - { - Stream streamResponse = myHttpWebResponse.GetResponseStream(); - var streamRead = new StreamReader(streamResponse); - var readBuff = new Char[256]; - int count = streamRead.Read(readBuff, 0, 256); - while (count > 0) - { - var outputData = new String(readBuff, 0, count); - retVal += outputData; - count = streamRead.Read(readBuff, 0, 256); - } - // Close the Stream object. - streamResponse.Close(); - streamRead.Close(); - - // Find the content of a form - string grabStart = ""; - string grabEnd = ""; - int grabStartPos = retVal.IndexOf(grabStart) + grabStart.Length; - int grabEndPos = retVal.IndexOf(grabEnd) - grabStartPos; - retVal = retVal.Substring(grabStartPos, grabEndPos); - } - else - retVal = ShowNoMacroContent(currentMacro); - - // Release the HttpWebResponse Resource. - myHttpWebResponse.Close(); - } - catch (Exception) - { - retVal = ShowNoMacroContent(currentMacro); - } - finally - { - // Release the HttpWebResponse Resource. - if (myHttpWebResponse != null) - myHttpWebResponse.Close(); - } - - return retVal.Replace("\n", string.Empty).Replace("\r", string.Empty); - } - - return ShowNoMacroContent(currentMacro); - } - - private static string ShowNoMacroContent(macro currentMacro) - { - return "" + currentMacro.Name + - "
No macro content available for WYSIWYG editing
"; - } - - private static bool ValidateRemoteCertificate( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors policyErrors - ) - { - if (GlobalSettings.DebugMode) - { - // allow any old dodgy certificate... - return true; - } - else - { - return policyErrors == SslPolicyErrors.None; - } - } - - /// - /// Adds the XSLT extension namespaces to the XSLT header using - /// {0} as the container for the namespace references and - /// {1} as the container for the exclude-result-prefixes - /// - /// The XSLT - /// - public static string AddXsltExtensionsToHeader(string xslt) - { - var namespaceList = new StringBuilder(); - var namespaceDeclaractions = new StringBuilder(); - foreach (var extension in GetXsltExtensions()) - { - namespaceList.Append(extension.Key).Append(' '); - namespaceDeclaractions.AppendFormat("xmlns:{0}=\"urn:{0}\" ", extension.Key); - } - - // parse xslt - xslt = xslt.Replace("{0}", namespaceDeclaractions.ToString()); - xslt = xslt.Replace("{1}", namespaceList.ToString()); - return xslt; - } - - #region Events - - /// - /// Occurs when a macro error is raised. - /// - public static event EventHandler Error; - - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnError(MacroErrorEventArgs e) - { - if (Error != null) - { - Error(this, e); - } - } - - #endregion - } - - /// - /// Event arguments used for the MacroRendering event - /// - public class MacroRenderingEventArgs : EventArgs - { - public Hashtable PageElements { get; private set; } - public int PageId { get; private set; } - - public MacroRenderingEventArgs(Hashtable pageElements, int pageId) - { - PageElements = pageElements; - PageId = pageId; - } - } - -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/template.cs b/src/Umbraco.Web/umbraco.presentation/template.cs index 436b7ad96a..bae00a070f 100644 --- a/src/Umbraco.Web/umbraco.presentation/template.cs +++ b/src/Umbraco.Web/umbraco.presentation/template.cs @@ -19,6 +19,7 @@ using umbraco.BusinessLogic; using Umbraco.Core.IO; using System.Web; using Umbraco.Core.Xml; +using Umbraco.Web.Macros; namespace umbraco { @@ -278,19 +279,20 @@ namespace umbraco } else { - macro tempMacro; + MacroModel tempMacro; String macroID = helper.FindAttribute(attributes, "macroid"); if (macroID != String.Empty) tempMacro = GetMacro(macroID); else - tempMacro = macro.GetMacro(helper.FindAttribute(attributes, "macroalias")); + tempMacro = MacroRenderer.GetMacroModel(helper.FindAttribute(attributes, "macroalias")); if (tempMacro != null) { try { - Control c = tempMacro.RenderMacro(attributes, umbPage.Elements, umbPage.PageID); + var renderer = new MacroRenderer(ApplicationContext.Current.ProfilingLogger); + var c = renderer.Render(tempMacro, umbPage.Elements, umbPage.PageID, attributes).GetAsControl(); if (c != null) pageContent.Controls.Add(c); else @@ -407,8 +409,9 @@ namespace umbraco String macroID = helper.FindAttribute(attributes, "macroid"); if (macroID != "") { - macro tempMacro = GetMacro(macroID); - _templateOutput.Replace(tag.Value.ToString(), tempMacro.MacroContent.ToString()); + // fixme - wtf? in 7.4 *nothing* ever writes to macro.MacroContent! + //macro tempMacro = GetMacro(macroID); + _templateOutput.Replace(tag.Value.ToString(), "" /*tempMacro.MacroContent.ToString()*/); } } else @@ -425,8 +428,9 @@ namespace umbraco String macroID = helper.FindAttribute(tempAttributes, "macroid"); if (Convert.ToInt32(macroID) > 0) { - macro tempContentMacro = GetMacro(macroID); - _templateOutput.Replace(tag.Value.ToString(), tempContentMacro.MacroContent.ToString()); + // fixme - wtf? in 7.4 *nothing* ever writes to macro.MacroContent! + //macro tempContentMacro = GetMacro(macroID); + _templateOutput.Replace(tag.Value.ToString(), "" /*tempContentMacro.MacroContent.ToString()*/); } } @@ -450,10 +454,11 @@ namespace umbraco #region private methods - private static macro GetMacro(String macroID) + private static MacroModel GetMacro(string macroId) { - System.Web.HttpContext.Current.Trace.Write("umbracoTemplate", "Starting macro (" + macroID.ToString() + ")"); - return macro.GetMacro(Convert.ToInt16(macroID)); + HttpContext.Current.Trace.Write("umbracoTemplate", "Starting macro (" + macroId + ")"); + var id = int.Parse(macroId); + return MacroRenderer.GetMacroModel(id); } #endregion diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/XsltTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/XsltTasks.cs index 4ed46500d0..a4040a4b5b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/XsltTasks.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/create/XsltTasks.cs @@ -51,9 +51,9 @@ namespace umbraco xsltFile.Close(); } - // prepare support for XSLT extensions - xslt = macro.AddXsltExtensionsToHeader(xslt); - var xsltWriter = System.IO.File.CreateText(xsltNewFilename); + // prepare support for XSLT extensions + xslt = Umbraco.Web.Macros.XsltMacroEngine.AddXsltExtensionsToHeader(xslt); + var xsltWriter = System.IO.File.CreateText(xsltNewFilename); xsltWriter.Write(xslt); xsltWriter.Flush(); xsltWriter.Close(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs index f44a69084b..bfcd1a1efe 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs @@ -61,7 +61,10 @@ namespace umbraco.presentation.developer.packages { ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearAllCache(); ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.ClearAllCaches(); - library.RefreshContent(); + + // library.RefreshContent is obsolete, would need to RefreshAllFacade, + // but it should be managed automatically by services and caches! + //DistributedCache.Instance.RefreshAllFacade(); loadNitros.Visible = false; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs index 6d81ea9f28..c00b0ab56e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs @@ -9,7 +9,6 @@ using Umbraco.Core.Services; using Umbraco.Core.Logging; using Umbraco.Core.Models; using umbraco.cms.businesslogic.web; -using runtimeMacro = umbraco.macro; using System.Xml; using umbraco.cms.presentation.Trees; using Umbraco.Web.UI.Pages; @@ -556,7 +555,9 @@ namespace umbraco.presentation.developer.packages // refresh cache if (refreshCache) { - library.RefreshContent(); + // library.RefreshContent is obsolete, would need to RefreshAllFacade, + // but it should be managed automatically by services and caches! + //DistributedCache.Instance.RefreshAllFacade(); } //ensure that all tree's are refreshed after uninstall diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs index 49fc01d1b7..9dd6fd85f9 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs @@ -217,7 +217,9 @@ namespace umbraco.presentation.developer.packages //making sure that publishing actions performed from the cms layer gets pushed to the presentation - library.RefreshContent(); + // library.RefreshContent is obsolete, would need to RefreshAllFacade, + // but it should be managed automatically by services and caches! + //DistributedCache.Instance.RefreshAllFacade(); if (string.IsNullOrEmpty(_installer.Control) == false) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltChooseExtension.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltChooseExtension.aspx.cs index 474feaecb1..52b6b78678 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltChooseExtension.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltChooseExtension.aspx.cs @@ -95,7 +95,7 @@ namespace umbraco.developer SortedList> _tempAssemblies = new SortedList>(); // add all extensions definied by macro - foreach(KeyValuePair extension in macro.GetXsltExtensions()) + foreach(KeyValuePair extension in Umbraco.Web.Macros.XsltMacroEngine.GetXsltExtensions()) _tempAssemblies.Add(extension.Key, GetStaticMethods(extension.Value.GetType())); // add the Umbraco library (not included in macro extensions) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltVisualize.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltVisualize.aspx.cs index 255c132187..5f54a23046 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltVisualize.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/xsltVisualize.aspx.cs @@ -33,43 +33,30 @@ namespace umbraco.presentation.umbraco.developer.Xslt protected void visualizeDo_Click(object sender, EventArgs e) { - // get xslt file - string xslt = ""; + string xslt; if (xsltSelection.Value.Contains("", xsltSelection.Value); - - // prepare support for XSLT extensions - xslt = macro.AddXsltExtensionsToHeader(xslt); - + xslt = Umbraco.Web.Macros.XsltMacroEngine.AddXsltExtensionsToHeader(xslt); } - Dictionary parameters = new Dictionary(1); - parameters.Add("currentPage", library.GetXmlNodeById(contentPicker.Value)); + int pageId; + if (int.TryParse(contentPicker.Value, out pageId) == false) + pageId = -1; - - // apply the XSLT transformation - string xsltResult = ""; - XmlTextReader xslReader = null; + // transform + string xsltResult; try { - xslReader = new XmlTextReader(new StringReader(xslt)); - System.Xml.Xsl.XslCompiledTransform xsl = macro.CreateXsltTransform(xslReader, false); - xsltResult = macro.GetXsltTransformResult(new XmlDocument(), xsl, parameters); + xsltResult = Umbraco.Web.Macros.XsltMacroEngine.TestXsltTransform(ApplicationContext.Current.ProfilingLogger, xslt, pageId); } catch (Exception ee) { @@ -77,10 +64,7 @@ namespace umbraco.presentation.umbraco.developer.Xslt "

Error parsing the XSLT:

{0}

", ee.ToString()); } - finally - { - xslReader.Close(); - } + visualizeContainer.Visible = true; // update output diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/Preview.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/Preview.aspx.cs index fc91d973c4..71f5f2e805 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/Preview.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/Preview.aspx.cs @@ -1,21 +1,8 @@ using System; -using System.Collections; -using System.Configuration; -using System.Data; -using System.Globalization; -using System.Linq; using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.HtmlControls; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Xml.Linq; using Umbraco.Web; -using umbraco.cms.businesslogic.web; -using umbraco.presentation.preview; -using umbraco.BusinessLogic; using Umbraco.Core; +using Umbraco.Web.PublishedCache; namespace umbraco.presentation.dialogs { @@ -23,19 +10,21 @@ namespace umbraco.presentation.dialogs { public Preview() { - CurrentApp = Constants.Applications.Content.ToString(); + CurrentApp = Constants.Applications.Content; } protected void Page_Load(object sender, EventArgs e) { - var d = new Document(Request.GetItemAs("id")); - var pc = new PreviewContent(Security.CurrentUser, Guid.NewGuid(), false); - pc.PrepareDocument(Security.CurrentUser, d, true); - pc.SavePreviewSet(); - docLit.Text = d.Text; - changeSetUrl.Text = pc.PreviewsetPath; - pc.ActivatePreviewCookie(); - Response.Redirect("../../" + d.Id.ToString(CultureInfo.InvariantCulture) + ".aspx", true); + var user = UmbracoContext.Security.CurrentUser; + var contentId = Request.GetItemAs("id"); + + var facadeService = FacadeServiceResolver.Current.Service; + var previewToken = facadeService.EnterPreview(user, contentId); + + UmbracoContext.HttpContext.Response.Cookies.Set(new HttpCookie(Constants.Web.PreviewCookieName, previewToken)); + + // use a numeric url because content may not be in cache and so .Url would fail + Response.Redirect($"../../{contentId}.aspx", true); } } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs deleted file mode 100644 index f5892d4fa3..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web; -using System.Xml; -using System.IO; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using umbraco.cms.businesslogic.web; -using Umbraco.Core.Models.Membership; -using Umbraco.Web; - -namespace umbraco.presentation.preview -{ - //TODO : Migrate this to a new API! - - [Obsolete("Get rid of this!!!")] - public class PreviewContent - { - // zb-00004 #29956 : refactor cookies names & handling - - public XmlDocument XmlContent { get; set; } - public Guid PreviewSet { get; set; } - public string PreviewsetPath { get; set; } - - public bool ValidPreviewSet { get; set; } - - private int _userId = -1; - - public PreviewContent() - { - _initialized = false; - } - - private readonly object _initLock = new object(); - private bool _initialized = true; - - public void EnsureInitialized(IUser user, string previewSet, bool validate, Action initialize) - { - lock (_initLock) - { - if (_initialized) return; - - _userId = user.Id; - ValidPreviewSet = UpdatePreviewPaths(new Guid(previewSet), validate); - initialize(); - _initialized = true; - } - } - - public PreviewContent(User user) - { - _userId = user.Id; - } - - public PreviewContent(Guid previewSet) - { - ValidPreviewSet = UpdatePreviewPaths(previewSet, true); - } - public PreviewContent(IUser user, Guid previewSet, bool validate) - { - _userId = user.Id; - ValidPreviewSet = UpdatePreviewPaths(previewSet, validate); - } - - - public void PrepareDocument(IUser user, Document documentObject, bool includeSubs) - { - _userId = user.Id; - - // clone xml - XmlContent = (XmlDocument)content.Instance.XmlContent.Clone(); - - var previewNodes = new List(); - - var parentId = documentObject.Level == 1 ? -1 : documentObject.ParentId; - - while (parentId > 0 && XmlContent.GetElementById(parentId.ToString(CultureInfo.InvariantCulture)) == null) - { - var document = new Document(parentId); - previewNodes.Insert(0, document); - parentId = document.ParentId; - } - - previewNodes.Add(documentObject); - - foreach (var document in previewNodes) - { - //Inject preview xml - parentId = document.Level == 1 ? -1 : document.ParentId; - var previewXml = document.ToPreviewXml(XmlContent); - if (document.ContentEntity.Published == false - && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) - previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - XmlContent = content.GetAddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); - } - - if (includeSubs) - { - foreach (var prevNode in documentObject.GetNodesForPreview(true)) - { - var previewXml = XmlContent.ReadNode(XmlReader.Create(new StringReader(prevNode.Xml))); - if (prevNode.IsDraft) - previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - XmlContent = content.GetAddOrUpdateXmlNode(XmlContent, prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml); - } - } - - } - - private bool UpdatePreviewPaths(Guid previewSet, bool validate) - { - if (_userId == -1) - { - throw new ArgumentException("No current Umbraco User registered in Preview", "m_userId"); - } - - PreviewSet = previewSet; - PreviewsetPath = GetPreviewsetPath(_userId, previewSet); - - if (validate && !ValidatePreviewPath()) - { - // preview cookie failed so we'll log the error and clear the cookie - LogHelper.Debug(string.Format("Preview failed for preview set {0} for user {1}", previewSet, _userId)); - - PreviewSet = Guid.Empty; - PreviewsetPath = String.Empty; - - ClearPreviewCookie(); - - return false; - } - - return true; - } - - private static string GetPreviewsetPath(int userId, Guid previewSet) - { - return IOHelper.MapPath( - Path.Combine(SystemDirectories.Preview, userId + "_" + previewSet + ".config")); - } - - /// - /// Checks a preview file exist based on preview cookie - /// - /// - public bool ValidatePreviewPath() - { - if (!File.Exists(PreviewsetPath)) - return false; - - return true; - } - - - - public void LoadPreviewset() - { - XmlContent = new XmlDocument(); - XmlContent.Load(PreviewsetPath); - } - - public void SavePreviewSet() - { - //make sure the preview folder exists first - var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview)); - if (!dir.Exists) - { - dir.Create(); - } - - // check for old preview sets and try to clean - CleanPreviewDirectory(_userId, dir); - - XmlContent.Save(PreviewsetPath); - } - - private static void CleanPreviewDirectory(int userId, DirectoryInfo dir) - { - foreach (FileInfo file in dir.GetFiles(userId + "_*.config")) - { - DeletePreviewFile(userId, file); - } - // also delete any files accessed more than 10 minutes ago - var now = DateTime.Now; - foreach (FileInfo file in dir.GetFiles("*.config")) - { - if ((now - file.LastAccessTime).TotalMinutes > 10) - DeletePreviewFile(userId, file); - } - } - - private static void DeletePreviewFile(int userId, FileInfo file) - { - try - { - file.Delete(); - } - catch (Exception ex) - { - LogHelper.Error(string.Format("Couldn't delete preview set: {0} - User {1}", file.Name, userId), ex); - } - } - - public void ActivatePreviewCookie() - { - HttpContext.Current.Response.Cookies.Set(new HttpCookie(Constants.Web.PreviewCookieName, PreviewSet.ToString())); - } - - public static void ClearPreviewCookie() - { - if (UmbracoContext.Current.Security.CurrentUser != null) - { - if (HttpContext.Current.Request.HasPreviewCookie()) - { - - DeletePreviewFile( - UmbracoContext.Current.Security.CurrentUser.Id, - new FileInfo(GetPreviewsetPath( - UmbracoContext.Current.Security.CurrentUser.Id, - new Guid(HttpContext.Current.Request.GetPreviewCookieValue())))); - } - } - HttpContext.Current.ExpireCookie(Constants.Web.PreviewCookieName); - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs index c77f0d8e35..c94982f5a7 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs @@ -9,6 +9,7 @@ using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Core.Models; using Umbraco.Web; +using Umbraco.Web.Macros; using Umbraco.Web._Legacy.Actions; namespace umbraco.presentation.templateControls @@ -227,7 +228,7 @@ namespace umbraco.presentation.templateControls { if (!String.IsNullOrEmpty(NodeId)) { - string tempNodeId = helper.parseAttribute(PageElements, NodeId); + string tempNodeId = MacroRenderer.ParseAttribute(PageElements, NodeId); int nodeIdInt = 0; if (int.TryParse(tempNodeId, out nodeIdInt)) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/ItemRenderer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/ItemRenderer.cs index 2dc9791cf7..442c52d1a3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/ItemRenderer.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/ItemRenderer.cs @@ -110,10 +110,14 @@ namespace umbraco.presentation.templateControls if (tempNodeId != null && tempNodeId.Value != 0) { //moved the following from the catch block up as this will allow fallback options alt text etc to work - var cache = Umbraco.Web.UmbracoContext.Current.ContentCache.InnerCache as PublishedContentCache; - if (cache == null) throw new InvalidOperationException("Unsupported IPublishedContentCache, only the Xml one is supported."); - var xml = cache.GetXml(Umbraco.Web.UmbracoContext.Current, Umbraco.Web.UmbracoContext.Current.InPreviewMode); - var itemPage = new page(xml.GetElementById(tempNodeId.ToString())); + // stop using GetXml + //var cache = Umbraco.Web.UmbracoContext.Current.ContentCache.InnerCache as PublishedContentCache; + //if (cache == null) throw new InvalidOperationException("Unsupported IPublishedContentCache, only the Xml one is supported."); + //var xml = cache.GetXml(Umbraco.Web.UmbracoContext.Current, Umbraco.Web.UmbracoContext.Current.InPreviewMode); + //var itemPage = new page(xml.GetElementById(tempNodeId.ToString())); + var c = Umbraco.Web.UmbracoContext.Current.ContentCache.GetById(tempNodeId.Value); + var itemPage = new page(c); + tempElementContent = new item(item.ContentItem, itemPage.Elements, item.LegacyAttributes).FieldContent; } @@ -212,7 +216,7 @@ namespace umbraco.presentation.templateControls // prepare support for XSLT extensions StringBuilder namespaceList = new StringBuilder(); StringBuilder namespaceDeclaractions = new StringBuilder(); - foreach (KeyValuePair extension in macro.GetXsltExtensions()) + foreach (KeyValuePair extension in Umbraco.Web.Macros.XsltMacroEngine.GetXsltExtensions()) { namespaceList.Append(extension.Key).Append(' '); namespaceDeclaractions.AppendFormat("xmlns:{0}=\"urn:{0}\" ", extension.Key); @@ -227,10 +231,11 @@ namespace umbraco.presentation.templateControls parameters.Add("itemData", itemData); // apply the XSLT transformation - XmlTextReader xslReader = new XmlTextReader(new StringReader(xslt)); - System.Xml.Xsl.XslCompiledTransform xsl = macro.CreateXsltTransform(xslReader, false); - itemData = macro.GetXsltTransformResult(new XmlDocument(), xsl, parameters); - xslReader.Close(); + using (var xslReader = new XmlTextReader(new StringReader(xslt))) + { + var transform = Umbraco.Web.Macros.XsltMacroEngine.GetXsltTransform(xslReader, false); + return Umbraco.Web.Macros.XsltMacroEngine.ExecuteItemRenderer(ApplicationContext.Current.ProfilingLogger, transform, itemData); + } } return itemData; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Macro.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Macro.cs index ad9cc6bed3..c9311bb1a0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Macro.cs @@ -6,9 +6,11 @@ using System.Web.UI.WebControls; using System.Collections; using umbraco.cms.businesslogic.macro; using System.Web; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web; +using Umbraco.Web.Macros; namespace umbraco.presentation.templateControls { @@ -149,39 +151,40 @@ namespace umbraco.presentation.templateControls int pageId = Context.Items["pageID"] == null ? int.MinValue : int.Parse(Context.Items["pageID"].ToString()); if ((!String.IsNullOrEmpty(Language) && Text != "") || !string.IsNullOrEmpty(FileLocation)) { - var tempMacro = new macro(); - tempMacro.GenerateMacroModelPropertiesFromAttributes(MacroAttributes); + var tempMacro = new MacroModel(); + MacroRenderer.GenerateMacroModelPropertiesFromAttributes(tempMacro, MacroAttributes); if (string.IsNullOrEmpty(FileLocation)) { - tempMacro.Model.ScriptCode = Text; - tempMacro.Model.ScriptLanguage = Language; + tempMacro.ScriptCode = Text; + tempMacro.ScriptLanguage = Language; } else { - tempMacro.Model.ScriptName = FileLocation; + tempMacro.ScriptName = FileLocation; } - tempMacro.Model.MacroType = MacroTypes.PartialView; + tempMacro.MacroType = MacroTypes.PartialView; if (!String.IsNullOrEmpty(Attributes["Cache"])) { var cacheDuration = 0; if (int.TryParse(Attributes["Cache"], out cacheDuration)) - tempMacro.Model.CacheDuration = cacheDuration; + tempMacro.CacheDuration = cacheDuration; else Context.Trace.Warn("Template", "Cache attribute is in incorect format (should be an integer)."); } - var c = tempMacro.RenderMacro((Hashtable)Context.Items["pageElements"], pageId); + var renderer = new MacroRenderer(ApplicationContext.Current.ProfilingLogger); + var c = renderer.Render(tempMacro, (Hashtable) Context.Items["pageElements"], pageId).GetAsControl(); if (c != null) { - Exceptions = tempMacro.Exceptions; - + Exceptions = renderer.Exceptions; Controls.Add(c); } else Context.Trace.Warn("Template", "Result of inline macro scripting is null"); } else { - var tempMacro = macro.GetMacro(Alias); + var tempMacro = MacroRenderer.GetMacroModel(Alias); if (tempMacro != null) { try { - var c = tempMacro.RenderMacro(MacroAttributes, (Hashtable)Context.Items["pageElements"], pageId); + var renderer = new MacroRenderer(ApplicationContext.Current.ProfilingLogger); + var c = renderer.Render(tempMacro, (Hashtable)Context.Items["pageElements"], pageId, MacroAttributes).GetAsControl(); if (c != null) Controls.Add(c); else diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs index 634a0edce5..4c732be52b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -56,7 +56,7 @@ namespace umbraco.presentation.webservices { var jsonRefresher = refresher as IJsonCacheRefresher; if (jsonRefresher == null) - throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.RefresherUniqueId + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); return jsonRefresher; } @@ -182,7 +182,7 @@ namespace umbraco.presentation.webservices foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) { var n = XmlHelper.AddTextNode(xd, "cacheRefresher", cr.Name); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); + n.Attributes.Append(XmlHelper.AddAttribute(xd, "uniqueIdentifier", cr.RefresherUniqueId.ToString())); xd.DocumentElement.AppendChild(n); } return xd; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs index 8a17236944..2d7c0d3f75 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs @@ -21,6 +21,7 @@ using umbraco.cms.businesslogic.template; using umbraco.cms.businesslogic.web; using System.Net; using System.Collections; +using Umbraco.Web.Macros; namespace umbraco.presentation.webservices { @@ -56,10 +57,14 @@ namespace umbraco.presentation.webservices // Test the xslt string errorMessage = ""; - if (!ignoreDebugging) + if (ignoreDebugging == false) { try { + if (UmbracoContext.ContentCache.HasContent()) + XsltMacroEngine.TestXsltTransform(ApplicationContext.ProfilingLogger, fileContents); + + /* // Check if there's any documents yet string xpath = "/root/*"; if (content.Instance.XmlContent.SelectNodes(xpath).Count > 0) @@ -98,6 +103,7 @@ namespace umbraco.presentation.webservices File.Delete(tempFileName); } } + */ else { //errorMessage = Services.TextService.Localize("developer/xsltErrorNoNodesPublished"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs index b5a763c7ce..28328f2d2c 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs @@ -17,6 +17,7 @@ using Umbraco.Web.WebServices; using umbraco.cms.businesslogic.web; using umbraco.cms.businesslogic.media; using Umbraco.Core.Models.Membership; +using Umbraco.Web.Macros; using Umbraco.Web._Legacy.UI; @@ -243,11 +244,14 @@ namespace umbraco.presentation.webservices // Test the xslt var errorMessage = ""; - if (!ignoreDebugging) + if (ignoreDebugging == false) { try { + if (UmbracoContext.ContentCache.HasContent()) + XsltMacroEngine.TestXsltTransform(ApplicationContext.ProfilingLogger, fileContents); + /* // Check if there's any documents yet if (content.Instance.XmlContent.SelectNodes("/root/node").Count > 0) { @@ -284,6 +288,7 @@ namespace umbraco.presentation.webservices macroResult.Close(); } } + */ else { errorMessage = "stub"; diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index 3091081e1f..8002060506 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -9,6 +9,8 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using umbraco.DataLayer; using System.Runtime.CompilerServices; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; using File = System.IO.File; using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; @@ -17,13 +19,13 @@ namespace umbraco.cms.businesslogic { /// /// Content is an intermediate layer between CMSNode and class'es which will use generic data. - /// + /// /// Content is a datastructure that holds generic data defined in its corresponding ContentType. Content can in some /// sence be compared to a row in a database table, it's contenttype hold a definition of the columns and the Content /// contains the data - /// + /// /// Note that Content data in umbraco is *not* tablular but in a treestructure. - /// + /// /// [Obsolete("Obsolete, Use Umbraco.Core.Models.Content or Umbraco.Core.Models.Media", false)] public class Content : CMSNode @@ -50,7 +52,7 @@ namespace umbraco.cms.businesslogic public Content(int id) : base(id) { } protected Content(int id, bool noSetup) : base(id, noSetup) { } - + protected Content(Guid id) : base(id) { } protected Content(Guid id, bool noSetup) : base(id, noSetup) { } @@ -158,10 +160,10 @@ namespace umbraco.cms.businesslogic /// /// This is here for performance reasons only. If the _contentTypeIcon is manually set /// then a database call is not made to initialize the ContentType. - /// + /// /// The data layer has slightly changed in 4.1 so that for Document and Media, the ContentType /// is automatically initialized with one SQL call when creating the documents/medias so using this - /// method or the ContentType.IconUrl property when accessing the icon from Media or Document + /// method or the ContentType.IconUrl property when accessing the icon from Media or Document /// won't affect performance. /// public string ContentTypeIcon @@ -185,36 +187,13 @@ namespace umbraco.cms.businesslogic { get { - if (!_versionDateInitialized) - { - // A Media item only contains a single version (which relates to it's creation) so get this value from the media xml fragment instead - if (this is media.Media) - { - // get the xml fragment from cmsXmlContent - string xmlFragment = SqlHelper.ExecuteScalar(@"SELECT [xml] FROM cmsContentXml WHERE nodeId = " + this.Id); - if (!string.IsNullOrWhiteSpace(xmlFragment)) - { - XmlDocument xmlDocument = new XmlDocument(); - xmlDocument.LoadXml(xmlFragment); + if (_versionDateInitialized) return _versionDate; - _versionDateInitialized = DateTime.TryParse(xmlDocument.SelectSingleNode("//*[1]").Attributes["updateDate"].Value, out _versionDate); - } - } - - if (!_versionDateInitialized) - { - object o = SqlHelper.ExecuteScalar( - "select VersionDate from cmsContentVersion where versionId = '" + this.Version.ToString() + "'"); - if (o == null) - { - _versionDate = DateTime.Now; - } - else - { - _versionDateInitialized = DateTime.TryParse(o.ToString(), out _versionDate); - } - } - } + // content & media have a version date in cmsContentVersion that is updated when saved - use it + var db = ApplicationContext.Current.DatabaseContext.Database; + _versionDate = db.ExecuteScalar("SELECT versionDate FROM cmsContentVersion WHERE versionId=@versionId", + new { @versionId = Version }); + _versionDateInitialized = true; return _versionDate; } set @@ -258,22 +237,11 @@ namespace umbraco.cms.businesslogic /// Used to persist object changes to the database. This ensures that the properties are re-loaded from the database. /// public override void Save() - { + { base.Save(); } - - - /// - /// Removes the Xml cached in the database - unpublish and cleaning - /// - public virtual void XmlRemoveFromDB() - { - SqlHelper.ExecuteNonQuery("delete from cmsContentXml where nodeId = @nodeId", SqlHelper.CreateParameter("@nodeId", this.Id)); - } - - /// /// Deletes the current Content object, must be overridden in the child class. /// @@ -283,15 +251,11 @@ namespace umbraco.cms.businesslogic // Delete all data associated with this content this.deleteAllProperties(); - // Remove all content preview xml - SqlHelper.ExecuteNonQuery("delete from cmsPreviewXml where nodeId = " + Id); + OnDeletedContent(new ContentDeleteEventArgs(ApplicationContext.Current.DatabaseContext.Database, Id)); // Delete version history SqlHelper.ExecuteNonQuery("Delete from cmsContentVersion where ContentId = " + this.Id); - // Delete xml - SqlHelper.ExecuteNonQuery("delete from cmsContentXml where nodeID = @nodeId", SqlHelper.CreateParameter("@nodeId", this.Id)); - // Delete Contentspecific data () SqlHelper.ExecuteNonQuery("Delete from cmsContent where NodeId = " + this.Id); @@ -337,29 +301,6 @@ namespace umbraco.cms.businesslogic _contentTypeIcon = InitContentTypeIcon; } - - - /// - /// Saves the XML document to the data source. - /// - /// The XML Document. - [MethodImpl(MethodImplOptions.Synchronized)] - protected virtual void SaveXmlDocument(XmlNode node) - { - // Method is synchronized so exists remains consistent (avoiding race condition) - bool exists = SqlHelper.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @nodeId", - SqlHelper.CreateParameter("@nodeId", Id)) > 0; - string query; - if (exists) - query = "UPDATE cmsContentXml SET xml = @xml WHERE nodeId = @nodeId"; - else - query = "INSERT INTO cmsContentXml(nodeId, xml) VALUES (@nodeId, @xml)"; - SqlHelper.ExecuteNonQuery(query, - SqlHelper.CreateParameter("@nodeId", Id), - SqlHelper.CreateParameter("@xml", node.OuterXml)); - } - - #endregion #region Private Methods @@ -376,6 +317,29 @@ namespace umbraco.cms.businesslogic #endregion - + #region Change Events + + // this is temp. until we get rid of Content + + internal protected class ContentDeleteEventArgs : EventArgs + { + public ContentDeleteEventArgs(UmbracoDatabase database, int id) + { + Database = database; + Id = id; + } + + public int Id { get; private set; } + public UmbracoDatabase Database { get; private set; } + } + + internal static event TypedEventHandler DeletedContent; + + internal protected void OnDeletedContent(ContentDeleteEventArgs args) + { + DeletedContent?.Invoke(this, args); + } + + #endregion } } \ No newline at end of file diff --git a/src/umbraco.cms/businesslogic/macro/Macro.cs b/src/umbraco.cms/businesslogic/macro/Macro.cs index 950f28812a..6c2510c622 100644 --- a/src/umbraco.cms/businesslogic/macro/Macro.cs +++ b/src/umbraco.cms/businesslogic/macro/Macro.cs @@ -429,25 +429,6 @@ namespace umbraco.cms.businesslogic.macro return MacroTypes.Unknown; } - public static string GenerateCacheKeyFromCode(string input) - { - if (String.IsNullOrEmpty(input)) - throw new ArgumentNullException("input", "An MD5 hash cannot be generated when 'input' parameter is null!"); - - // step 1, calculate MD5 hash from input - var md5 = MD5.Create(); - var inputBytes = Encoding.ASCII.GetBytes(input); - var hash = md5.ComputeHash(inputBytes); - - // step 2, convert byte array to hex string - var sb = new StringBuilder(); - for (var i = 0; i < hash.Length; i++) - { - sb.Append(hash[i].ToString("X2")); - } - return sb.ToString(); - } - #region Macro Refactor private static string GetCacheKey(string alias) diff --git a/src/umbraco.cms/businesslogic/macro/MacroModel.cs b/src/umbraco.cms/businesslogic/macro/MacroModel.cs deleted file mode 100644 index 2a619b7f5e..0000000000 --- a/src/umbraco.cms/businesslogic/macro/MacroModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Umbraco.Core.Models; - -namespace umbraco.cms.businesslogic.macro -{ - - [Serializable] - public class MacroModel - { - public int Id { get; set; } - public string Name { get; set; } - public string Alias { get; set; } - public string MacroControlIdentifier { get; set; } - public MacroTypes MacroType { get; set; } - - public string TypeName { get; set; } - public string Xslt { get; set; } - public string ScriptName { get; set; } - public string ScriptCode { get; set; } - public string ScriptLanguage { get; set; } - - public int CacheDuration { get; set; } - public bool CacheByPage { get; set; } - public bool CacheByMember { get; set; } - - public bool RenderInEditor { get; set; } - - public string CacheIdentifier { get; set; } - - public List Properties { get; set; } - - public MacroModel() - { - Properties = new List(); - } - - public MacroModel(Macro m) - { - Properties = new List(); - if (m != null) - { - Id = m.Id; - Name = m.Name; - Alias = m.Alias; - TypeName = m.Type; - Xslt = m.Xslt; - ScriptName = m.ScriptingFile; - CacheDuration = m.RefreshRate; - CacheByPage = m.CacheByPage; - CacheByMember = m.CachePersonalized; - RenderInEditor = m.RenderContent; - foreach (MacroProperty mp in m.Properties) - { - Properties.Add( - new MacroPropertyModel(mp.Alias, string.Empty, mp.Type.Alias, mp.Type.BaseType)); - } - MacroType = Macro.FindMacroType(Xslt, ScriptName, TypeName); - } - } - - public MacroModel(string name, string alias, string typeName, string xslt, string scriptName, int cacheDuration, bool cacheByPage, bool cacheByMember) - { - Name = name; - Alias = alias; - TypeName = typeName; - Xslt = xslt; - ScriptName = scriptName; - CacheDuration = cacheDuration; - CacheByPage = cacheByPage; - CacheByMember = cacheByMember; - - Properties = new List(); - - MacroType = Macro.FindMacroType(Xslt, ScriptName, TypeName); - } - } -} diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index bd2127bb77..f2f2f715c9 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -170,7 +170,6 @@ -