From 7747b8f64f9cb7d831bb9a65a6e462044226e2a9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 19 Nov 2013 09:16:54 +0100 Subject: [PATCH 01/23] U4-3579 - support chars such as <, > or & in prevalues --- src/Umbraco.Web.UI/umbraco/developer/DataTypes/editDatatype.aspx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI/umbraco/developer/DataTypes/editDatatype.aspx b/src/Umbraco.Web.UI/umbraco/developer/DataTypes/editDatatype.aspx index 86066a5c63..12f830e9eb 100644 --- a/src/Umbraco.Web.UI/umbraco/developer/DataTypes/editDatatype.aspx +++ b/src/Umbraco.Web.UI/umbraco/developer/DataTypes/editDatatype.aspx @@ -1,4 +1,5 @@ <%@ Page Language="c#" MasterPageFile="../../masterpages/umbracoPage.Master" Title="Edit data type" + ValidateRequest="false" CodeBehind="editDatatype.aspx.cs" AutoEventWireup="True" Inherits="umbraco.cms.presentation.developer.editDatatype" %> <%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> From 026d040a622aaf842526065ae7b63fbe1ae18da3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 6 Nov 2013 11:50:22 +0100 Subject: [PATCH 02/23] Enable StandaloneApplication to work with Powershell --- .../Standalone/PowershellAssemblyResolver.cs | 41 +++ .../Standalone/StandaloneApplication.cs | 26 +- .../Standalone/WriteableConfigSystem.cs | 262 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 4 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs create mode 100644 src/Umbraco.Web/Standalone/WriteableConfigSystem.cs diff --git a/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs new file mode 100644 index 0000000000..8c323902c0 --- /dev/null +++ b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Umbraco.Web.Standalone +{ + public static class PowershellAssemblyResolver + { + private static readonly Dictionary Assemblies; + private static readonly object Locko = new object(); + + static PowershellAssemblyResolver() + { + var comparer = StringComparer.CurrentCultureIgnoreCase; + Assemblies = new Dictionary(comparer); + AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler; + } + + public static void AddAssemblyLocation(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Arg is null or empty.", "path"); + + lock (Locko) + { + var name = Path.GetFileNameWithoutExtension(path); + Assemblies.Add(name, path); + } + } + + private static Assembly ResolveHandler(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + string assemblyFile; + return Assemblies.TryGetValue(assemblyName.Name, out assemblyFile) + ? Assembly.LoadFrom(assemblyFile) + : null; + } + } +} diff --git a/src/Umbraco.Web/Standalone/StandaloneApplication.cs b/src/Umbraco.Web/Standalone/StandaloneApplication.cs index 64e2975dc2..e490684dc7 100644 --- a/src/Umbraco.Web/Standalone/StandaloneApplication.cs +++ b/src/Umbraco.Web/Standalone/StandaloneApplication.cs @@ -59,7 +59,15 @@ namespace Umbraco.Web.Standalone if (noerr) return; throw new InvalidOperationException("Application has already started."); } - Application_Start(this, EventArgs.Empty); + try + { + Application_Start(this, EventArgs.Empty); + } + catch + { + TerminateInternal(); + throw; + } _started = true; } } @@ -74,14 +82,24 @@ namespace Umbraco.Web.Standalone throw new InvalidOperationException("Application has already been terminated."); } + TerminateInternal(); + } + } + + private void TerminateInternal() + { + if (ApplicationContext.Current != null) + { ApplicationContext.Current.DisposeIfDisposable(); // should reset resolution, clear caches & resolvers... ApplicationContext.Current = null; + } + if (UmbracoContext.Current != null) + { UmbracoContext.Current.DisposeIfDisposable(); // dunno UmbracoContext.Current = null; - - _started = false; - _application = null; } + _started = false; + _application = null; } #endregion diff --git a/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs new file mode 100644 index 0000000000..86d187c393 --- /dev/null +++ b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Specialized; +using System.Configuration; +using System.Configuration.Internal; +using System.Reflection; +using System.Threading; + +namespace Umbraco.Web.Standalone +{ + // see http://stackoverflow.com/questions/15653621/how-to-update-add-modify-delete-keys-in-appsettings-section-of-web-config-at-r + // see http://www.codeproject.com/Articles/69364/Override-Configuration-Manager + + public sealed class WriteableConfigSystem : IInternalConfigSystem + { + private static readonly ReaderWriterLockSlim RwLock = new ReaderWriterLockSlim(); + private static WriteableConfigSystem _installed; + private static IInternalConfigSystem _clientConfigSystem; + private object _appsettings; + private object _connectionStrings; + private static object _sInitStateOrig; + private static object _sConfigSystemOrig; + + public static bool Installed + { + get + { + try + { + RwLock.EnterReadLock(); + return _installed != null; + } + finally + { + RwLock.ExitReadLock(); + } + } + } + + /// + /// Re-initializes the ConfigurationManager, allowing us to merge in the settings from Core.Config + /// + public static void Install() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed != null) + throw new InvalidOperationException("ConfigSystem is already installed."); + + FieldInfo[] fiStateValues = null; + var tInitState = typeof(ConfigurationManager).GetNestedType("InitState", BindingFlags.NonPublic); + + if (tInitState != null) + fiStateValues = tInitState.GetFields(); + // 0: NotStarted + // 1: Started + // 2: Usable + // 3: Completed + + var fiInit = typeof(ConfigurationManager).GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static); + var fiSystem = typeof(ConfigurationManager).GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static); + + if (fiInit != null && fiSystem != null && fiStateValues != null) + { + _sInitStateOrig = fiInit.GetValue(null); + _sConfigSystemOrig = fiSystem.GetValue(null); + fiInit.SetValue(null, fiStateValues[1].GetValue(null)); // set to Started + fiSystem.SetValue(null, null); // clear current config system + } + + _installed = new WriteableConfigSystem(); + + var configFactoryType = Type.GetType("System.Configuration.Internal.InternalConfigSettingsFactory, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true); + var configSettingsFactory = (IInternalConfigSettingsFactory)Activator.CreateInstance(configFactoryType, true); + // just does ConfigurationManager.SetConfigurationSystem(_installed, false); + // 'false' turns initState to 2 ie usable (vs 3 ie completed) + configSettingsFactory.SetConfigurationSystem(_installed, false); + + // note: prob. don't need the factory... see how we uninstall... + + var clientConfigSystemType = Type.GetType("System.Configuration.ClientConfigurationSystem, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true); + _clientConfigSystem = (IInternalConfigSystem)Activator.CreateInstance(clientConfigSystemType, true); + } + finally + { + RwLock.ExitWriteLock(); + } + } + + public static void Uninstall() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed == null) + throw new InvalidOperationException("ConfigSystem is not installed."); + + FieldInfo[] fiStateValues = null; + var tInitState = typeof(ConfigurationManager).GetNestedType("InitState", BindingFlags.NonPublic); + + if (tInitState != null) + fiStateValues = tInitState.GetFields(); + + var fiInit = typeof(ConfigurationManager).GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static); + var fiSystem = typeof(ConfigurationManager).GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static); + + if (fiInit != null && fiSystem != null && fiStateValues != null) + { + // reset - the hard way + fiInit.SetValue(null, _sInitStateOrig); + fiSystem.SetValue(null, _sConfigSystemOrig); + } + + _installed = null; + _clientConfigSystem = null; + } + finally + { + RwLock.ExitWriteLock(); + } + } + + public static void Reset() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed == null) + throw new InvalidOperationException("ConfigSystem is not installed."); + + _installed._appsettings = null; + _installed._connectionStrings = null; + } + finally + { + RwLock.ExitWriteLock(); + } + } + + #region IInternalConfigSystem Members + + public object GetSection(string configKey) + { + // get the section from the default location (web.config or app.config) + var section = _clientConfigSystem.GetSection(configKey); + + try + { + RwLock.EnterReadLock(); + + switch (configKey) + { + case "appSettings": + // Return cached version if exists + if (_appsettings != null) + return _appsettings; + + // create a new collection because the underlying collection is read-only + var cfg = new NameValueCollection(); + + // If an AppSettings section exists in Web.config, read and add values from it + var nvSection = section as NameValueCollection; + if (nvSection != null) + { + var localSettings = nvSection; + foreach (string key in localSettings) + cfg.Add(key, localSettings[key]); + } + + //// -------------------------------------------------------------------- + //// Here I read and decrypt keys and add them to secureConfig dictionary + //// To test assume the following line is a key stored in secure sotrage. + ////secureConfig = SecureConfig.LoadConfig(); + //secureConfig.Add("ACriticalKey", "VeryCriticalValue"); + //// -------------------------------------------------------------------- + //foreach (KeyValuePair item in secureConfig) + //{ + // if (cfg.AllKeys.Contains(item.Key)) + // { + // cfg[item.Key] = item.Value; + // } + // else + // { + // cfg.Add(item.Key, item.Value); + // } + //} + //// -------------------------------------------------------------------- + + + // Cach the settings for future use + + _appsettings = cfg; + // return the merged version of the items from secure storage and appsettings + section = _appsettings; + break; + + case "connectionStrings": + // Return cached version if exists + if (_connectionStrings != null) + return _connectionStrings; + + // create a new collection because the underlying collection is read-only + var connectionStringsSection = new ConnectionStringsSection(); + + // copy the existing connection strings into the new collection + foreach ( + ConnectionStringSettings connectionStringSetting in + ((ConnectionStringsSection)section).ConnectionStrings) + connectionStringsSection.ConnectionStrings.Add(connectionStringSetting); + + // -------------------------------------------------------------------- + // Again Load connection strings from secure storage and merge like below + // connectionStringsSection.ConnectionStrings.Add(connectionStringSetting); + // -------------------------------------------------------------------- + + // Cach the settings for future use + _connectionStrings = connectionStringsSection; + // return the merged version of the items from secure storage and appsettings + section = _connectionStrings; + break; + } + } + finally + { + RwLock.ExitReadLock(); + } + + return section; + } + + public void RefreshConfig(string sectionName) + { + try + { + RwLock.EnterWriteLock(); + + if (sectionName == "appSettings") + { + _appsettings = null; + } + + if (sectionName == "connectionStrings") + { + _connectionStrings = null; + } + } + finally + { + RwLock.ExitWriteLock(); + } + + _clientConfigSystem.RefreshConfig(sectionName); + } + + public bool SupportsUserConfig { get { return _clientConfigSystem.SupportsUserConfig; } } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 944d51536a..f1a6f79d57 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -321,6 +321,7 @@ + @@ -396,6 +397,7 @@ + From 2f953c3819e86a74848aa01767fd241b914b53f9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 12 Feb 2014 16:40:42 +0100 Subject: [PATCH 03/23] Fix a38e0dc - use UInt64 instead of int --- src/Umbraco.Core/Models/UmbracoEntity.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 6822582576..7abffc913d 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -54,7 +54,8 @@ namespace Umbraco.Core.Models Trashed = trashed; } - public UmbracoEntity(int trashed) + // for MySql + public UmbracoEntity(UInt64 trashed) { AdditionalData = new Dictionary(); Trashed = trashed == 1; From 83736bec0170e938bc02e17c851e4c49cdee7867 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 4 Mar 2014 17:28:24 +0100 Subject: [PATCH 04/23] U4-4190 - enable retrieval of wildcard domains --- src/umbraco.cms/businesslogic/web/Domain.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/umbraco.cms/businesslogic/web/Domain.cs b/src/umbraco.cms/businesslogic/web/Domain.cs index 4547078673..8c3e630285 100644 --- a/src/umbraco.cms/businesslogic/web/Domain.cs +++ b/src/umbraco.cms/businesslogic/web/Domain.cs @@ -143,7 +143,7 @@ namespace umbraco.cms.businesslogic.web return GetDomains(false); } - internal static IEnumerable GetDomains(bool includeWildcards) + public static IEnumerable GetDomains(bool includeWildcards) { var domains = ApplicationContext.Current.ApplicationCache.GetCacheItem( CacheKeys.DomainCacheKey, @@ -191,6 +191,11 @@ namespace umbraco.cms.businesslogic.web return GetDomains().Where(d => d._root == nodeId).ToArray(); } + public static Domain[] GetDomainsById(int nodeId, bool includeWildcards) + { + return GetDomains(includeWildcards).Where(d => d._root == nodeId).ToArray(); + } + public static bool Exists(string DomainName) { return GetDomain(DomainName) != null; From 5dbc72897a366c9d9cba2276eb38cc3e1637fef5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 4 Mar 2014 13:01:48 +0100 Subject: [PATCH 05/23] Add method to reset PublishedContentRequest template --- src/Umbraco.Web/Routing/PublishedContentRequest.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 424ffa1b52..9ab312e33e 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -312,6 +312,16 @@ namespace Umbraco.Web.Routing TemplateModel = template; } + /// + /// Resets the template. + /// + /// The RenderingEngine becomes unknown. + public void ResetTemplate() + { + EnsureWriteable(); + TemplateModel = null; + } + /// /// Gets a value indicating whether the content request has a template. /// From ff0bcffefa303ad8ef543f66533d11fdc0ea6ca3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 12 Feb 2014 23:27:48 +0100 Subject: [PATCH 06/23] Implement DefaultShortStringHelper.PostFilter to cut url segments at 240 chars --- .../Strings/DefaultShortStringHelper.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index 07f262c272..508e356f71 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -115,6 +115,11 @@ namespace Umbraco.Core.Strings return InvalidFileNameChars.Contains(c) == false; } + public static string CutMaxLength(string text, int length) + { + return text.Length <= length ? text : text.Substring(0, length); + } + #endregion #region Configuration @@ -168,6 +173,7 @@ namespace Umbraco.Core.Strings return WithConfig(CleanStringType.UrlSegment, new Config { PreFilter = ApplyUrlReplaceCharacters, + PostFilter = x => CutMaxLength(x, 240), IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore StringType = (UrlReplacingToAscii ? CleanStringType.Ascii : CleanStringType.Utf8) | CleanStringType.LowerCase, BreakTermsOnUpper = false, @@ -202,6 +208,7 @@ namespace Umbraco.Core.Strings { StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; PreFilter = null; + PostFilter = null; IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); BreakTermsOnUpper = false; CutAcronymOnNonUpper = false; @@ -214,6 +221,7 @@ namespace Umbraco.Core.Strings return new Config { PreFilter = PreFilter, + PostFilter = PostFilter, IsTerm = IsTerm, StringType = StringType, BreakTermsOnUpper = BreakTermsOnUpper, @@ -224,6 +232,7 @@ namespace Umbraco.Core.Strings } public Func PreFilter { get; set; } + public Func PostFilter { get; set; } public Func IsTerm { get; set; } public CleanStringType StringType { get; set; } @@ -554,6 +563,10 @@ function validateSafeAlias(id, value, immediate, callback) {{ // clean text = CleanCodeString(text, stringType, separator.Value, culture, config); + // apply post-filter + if (config.PostFilter != null) + text = config.PostFilter(text); + return text; } From cec4d41df7b20de321e22a06e2d26255ee91bca8 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 4 Mar 2014 11:58:37 +0100 Subject: [PATCH 07/23] Add comments to UmbracoViewPageOfTModel --- src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 373e103a56..b7220b7cc1 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -141,6 +141,7 @@ namespace Umbraco.Web.Mvc // maps model protected override void SetViewData(ViewDataDictionary viewData) { + // if view data contains no model, nothing to do var source = viewData.Model; if (source == null) { @@ -148,6 +149,8 @@ namespace Umbraco.Web.Mvc return; } + // get the type of the view data model (what we have) + // get the type of this view model (what we want) var sourceType = source.GetType(); var targetType = typeof (TModel); @@ -160,13 +163,15 @@ namespace Umbraco.Web.Mvc // try to grab the content // if no content is found, return, nothing we can do - var sourceContent = source as IPublishedContent; + var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent if (sourceContent == null && sourceType.Implements()) { + // else check if it's an IRenderModel => get the content sourceContent = ((IRenderModel)source).Content; } if (sourceContent == null) { + // else check if we can convert it to a content var attempt = source.TryConvertTo(); if (attempt.Success) sourceContent = attempt.Result; } From 3b6dcc3421d2fe17d2d36f146fdcb108d80e5eb0 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 26 Jan 2014 14:59:35 +0100 Subject: [PATCH 08/23] Factor Dictionary.ReplaceKey method --- src/umbraco.cms/businesslogic/Dictionary.cs | 19 ++++++++++++++ .../dropdownlist/dropdown.cs | 25 ++----------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index e524f90429..cd32162fa7 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -532,5 +532,24 @@ namespace umbraco.cms.businesslogic } #endregion } + + // zb023 - utility method + public static string ReplaceKey(string text) + { + if (text.StartsWith("#") == false) + return text; + + var lang = Language.GetByCultureCode(Thread.CurrentThread.CurrentCulture.Name); + + if (lang == null) + return "[" + text + "]"; + + if (DictionaryItem.hasKey(text.Substring(1, text.Length - 1)) == false) + return "[" + text + "]"; + + var di = new DictionaryItem(text.Substring(1, text.Length - 1)); + return di.Value(lang.id); + } + } } \ No newline at end of file diff --git a/src/umbraco.editorControls/dropdownlist/dropdown.cs b/src/umbraco.editorControls/dropdownlist/dropdown.cs index f4d887cfb4..2cc44ae6a9 100644 --- a/src/umbraco.editorControls/dropdownlist/dropdown.cs +++ b/src/umbraco.editorControls/dropdownlist/dropdown.cs @@ -59,7 +59,7 @@ namespace umbraco.editorControls { foreach (object key in _prevalues.Keys) { - this.Items.Add(new ListItem(dropdown.DictionaryReplace(_prevalues[key].ToString()), key.ToString())); + this.Items.Add(new ListItem(Dictionary.ReplaceKey(_prevalues[key].ToString()), key.ToString())); } } @@ -67,7 +67,7 @@ namespace umbraco.editorControls { foreach (KeyValuePair item in Prevalues) { - this.Items.Add(new ListItem(dropdown.DictionaryReplace(item.Value), item.Key.ToString())); + this.Items.Add(new ListItem(Dictionary.ReplaceKey(item.Value), item.Key.ToString())); } } @@ -76,26 +76,5 @@ namespace umbraco.editorControls if (_data != null && _data.Value != null) this.SelectedValue = _data.Value.ToString(); } - - static string DictionaryReplace(string text) - { - if (!text.StartsWith("#")) - return text; - else - { - Language lang = Language.GetByCultureCode(System.Threading.Thread.CurrentThread.CurrentCulture.Name); - if (lang != null) - { - if (Dictionary.DictionaryItem.hasKey(text.Substring(1, text.Length - 1))) - { - Dictionary.DictionaryItem di = new Dictionary.DictionaryItem(text.Substring(1, text.Length - 1)); - return di.Value(lang.id); - } - } - - return "[" + text + "]"; - } - } - } } \ No newline at end of file From acd377956f14e903ae277b0f4a7c3a7498668291 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 5 Mar 2014 13:37:03 +0100 Subject: [PATCH 09/23] Improve how DomainHelper determines domain --- src/Umbraco.Web/Routing/DomainHelper.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 85a1c5f66f..9f64b51e04 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -152,12 +152,21 @@ namespace Umbraco.Web.Routing } else { - // look for the first domain that would be the base of the hint - var hintWithSlash = current.EndPathWithSlash(); + // look for the first domain that would be the base of the current url + // ie current is www.example.com/foo/bar, look for domain www.example.com + var currentWithSlash = current.EndPathWithSlash(); domainAndUri = domainsAndUris - .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash)); + .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash)); + if (domainAndUri != null) return domainAndUri; + + // look for the first domain that the current url would be the base of + // ie current is www.example.com, look for domain www.example.com/foo/bar + domainAndUri = domainsAndUris + .FirstOrDefault(d => currentWithSlash.IsBaseOf(d.Uri.EndPathWithSlash())); + if (domainAndUri != null) return domainAndUri; + // if none matches, then try to run the filter to pick a domain - if (domainAndUri == null && filter != null) + if (filter != null) { domainAndUri = filter(domainsAndUris); // if still nothing, pick the first one? From 3d4409e52a806793a70b48849f12c56286a631ed Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 27 Jan 2014 14:27:53 +0100 Subject: [PATCH 10/23] Refactor CacheRefreshersResolver.CacheResolvers into .CacheRefreshers --- src/Umbraco.Core/CacheRefreshersResolver.cs | 2 +- .../umbraco.presentation/umbraco/cache/LegacyClasses.cs | 2 +- .../umbraco/webservices/CacheRefresher.asmx.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/CacheRefreshersResolver.cs b/src/Umbraco.Core/CacheRefreshersResolver.cs index e01dfba01a..e8df98fc1b 100644 --- a/src/Umbraco.Core/CacheRefreshersResolver.cs +++ b/src/Umbraco.Core/CacheRefreshersResolver.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core /// /// Gets the implementations. /// - public IEnumerable CacheResolvers + public IEnumerable CacheRefreshers { get { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/cache/LegacyClasses.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/cache/LegacyClasses.cs index a497c86884..ad00646305 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/cache/LegacyClasses.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/cache/LegacyClasses.cs @@ -52,7 +52,7 @@ namespace umbraco.presentation.cache /// public ICacheRefresher[] GetAll() { - return CacheRefreshersResolver.Current.CacheResolvers.ToArray(); + return CacheRefreshersResolver.Current.CacheRefreshers.ToArray(); } #endregion 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 7f79097615..4c2f721bd2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -115,7 +115,7 @@ namespace umbraco.presentation.webservices { var xd = new XmlDocument(); xd.LoadXml(""); - foreach (var cr in CacheRefreshersResolver.Current.CacheResolvers) + 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())); From 792ab8d088a873aaaf04c9421db92fa93fa48c16 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 3 Mar 2014 13:29:33 +0100 Subject: [PATCH 11/23] Introduce support for content fragments --- .../PublishedContentTestElements.cs | 6 + .../PublishedCache/IPublishedContentCache.cs | 48 +++++ .../PublishedContentCache.cs | 10 ++ .../XmlPublishedCache/PublishedFragment.cs | 169 ++++++++++++++++++ .../PublishedFragmentProperty.cs | 43 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 6 files changed, 278 insertions(+) create mode 100644 src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs create mode 100644 src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index 3f3fa0a926..be357c4c03 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -99,6 +99,12 @@ namespace Umbraco.Tests.PublishedContent { return _content.Count > 0; } + + public IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + throw new NotImplementedException(); + } } class SolidPublishedContent : IPublishedContent diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index a80aaa41b3..f020dc6097 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -33,5 +33,53 @@ namespace Umbraco.Web.PublishedCache /// The route. /// The value of overrides the context. string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId); + + /// + /// Creates a content fragment. + /// + /// The content type alias. + /// The content property raw values. + /// A value indicating whether the fragment is previewing. + /// A value indicating whether the fragment is managed by the cache. + /// The newly created content fragment. + // + // notes + // + // in XmlPublishedCache, IPublishedContent instances are not meant to survive longer + // that a request or else we cannot guarantee that the converted property values will + // be properly managed - because XmlPublishedProperty just stores the result of the + // conversion locally. + // + // in DrippingPublishedCache, IPublishedContent instances are meant to survive for as + // long as the content itself has not been modified, and the property respects the + // converter's indication ie whether the converted value should be cached at + // .Content - cache until the content changes + // .ContentCache - cache until any content changes + // .Request - cache for the current request + // + // a fragment can be either "detached" or "managed". + // detached: created from code, managed by code, converted property values are + // cached within the fragment itself for as long as the fragment lives + // managed: created from a property converter as part of a content, managed by + // the cache, converted property values can be cached... + // + // XmlPublishedCache: same as content properties, store the result of the + // conversion locally, neither content nor fragments should survive longer + // than a request + // DrippingPublishedCache: depends + // .Content: cache within the fragment + // .ContentCache, .Request: cache within the cache + // + // in the latter case, use a fragment-owned guid as the cache key. because we + // don't really have any other choice. this opens potential memory leaks: if the + // fragment is re-created on each request and has a property that caches its + // converted value at .ContentCache level then we'll flood that cache with data + // that's never removed (as long as no content is edited). + // + // so a requirement should be that any converter that creates fragment, should + // be marked .Content -- and nothing else + // + IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed); } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 82a5b6f85a..2fe9c13c21 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -460,5 +460,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } #endregion + + #region Fragments + + public IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + return new PublishedFragment(contentTypeAlias, dataValues, isPreviewing, managed); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs new file mode 100644 index 0000000000..860f9f043f --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PublishedFragment : PublishedContentBase + { + private readonly PublishedContentType _contentType; + private readonly IPublishedProperty[] _properties; + + public PublishedFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + IsPreviewing = isPreviewing; + _contentType = PublishedContentType.Get(PublishedItemType.Content, contentTypeAlias); + + // we don't care about managed because in both cases, XmlPublishedCache stores + // converted property values in the IPublishedContent, which is not meant to + // survive the request + + var dataValues2 = new Dictionary(); + foreach (var kvp in dataValues) + dataValues2[kvp.Key.ToLowerInvariant()] = kvp.Value; + + _properties = _contentType.PropertyTypes + .Select(x => + { + object dataValue; + return dataValues2.TryGetValue(x.PropertyTypeAlias.ToLowerInvariant(), out dataValue) + ? new PublishedFragmentProperty(x, this, dataValue) + : new PublishedFragmentProperty(x, this); + }) + .Cast() + .ToArray(); + } + + #region IPublishedContent + + public override int Id + { + get { throw new NotImplementedException(); } + } + + public override int DocumentTypeId + { + get { return _contentType.Id; } + } + + public override string DocumentTypeAlias + { + get { return _contentType.Alias; } + } + + public override PublishedItemType ItemType + { + get { return PublishedItemType.Content; } + } + + public override string Name + { + get { throw new NotImplementedException(); } + } + + public override int Level + { + get { throw new NotImplementedException(); } + } + + public override string Path + { + get { throw new NotImplementedException(); } + } + + public override int SortOrder + { + // note - could a published fragment have a sort order? + get { throw new NotImplementedException(); } + } + + public override Guid Version + { + get { throw new NotImplementedException(); } + } + + public override int TemplateId + { + get { throw new NotImplementedException(); } + } + + public override string UrlName + { + get { return string.Empty; } + } + + public override DateTime CreateDate + { + get { throw new NotImplementedException(); } + } + + public override DateTime UpdateDate + { + get { throw new NotImplementedException(); } + } + + public override int CreatorId + { + get { throw new NotImplementedException(); } + } + + public override string CreatorName + { + get { throw new NotImplementedException(); } + } + + public override int WriterId + { + get { throw new NotImplementedException(); } + } + + public override string WriterName + { + get { throw new NotImplementedException(); } + } + + public override bool IsDraft + { + get { throw new NotImplementedException(); } + } + + public override IPublishedContent Parent + { + get { throw new NotImplementedException(); } + } + + public override IEnumerable Children + { + get { throw new NotImplementedException(); } + } + + public override ICollection Properties + { + get { return _properties; } + } + + public override IPublishedProperty GetProperty(string alias) + { + return _properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + public override PublishedContentType ContentType + { + get { return _contentType; } + } + + #endregion + + #region Internal + + // used by PublishedFragmentProperty + internal bool IsPreviewing { get; private set; } + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs new file mode 100644 index 0000000000..7ae10aebdd --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs @@ -0,0 +1,43 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PublishedFragmentProperty : PublishedPropertyBase + { + private readonly object _dataValue; + private readonly PublishedFragment _content; + + private readonly Lazy _sourceValue; + private readonly Lazy _objectValue; + private readonly Lazy _xpathValue; + + public PublishedFragmentProperty(PublishedPropertyType propertyType, PublishedFragment content) + : this(propertyType, content, null) + { } + + public PublishedFragmentProperty(PublishedPropertyType propertyType, PublishedFragment content, object dataValue) + : base(propertyType) + { + _dataValue = dataValue; + _content = content; + + _sourceValue = new Lazy(() => PropertyType.ConvertDataToSource(_dataValue, _content.IsPreviewing)); + _objectValue = new Lazy(() => PropertyType.ConvertSourceToObject(_sourceValue.Value, _content.IsPreviewing)); + _xpathValue = new Lazy(() => PropertyType.ConvertSourceToXPath(_sourceValue.Value, _content.IsPreviewing)); + } + + public override bool HasValue + { + get { return _dataValue != null && ((_dataValue is string) == false || string.IsNullOrWhiteSpace((string)_dataValue) == false); } + } + + public override object DataValue + { + get { return _dataValue; } + } + + public override object Value { get { return _objectValue.Value; } } + public override object XPathValue { get { return _xpathValue.Value; } } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f1a6f79d57..130a6d0fbf 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -298,6 +298,8 @@ + + From ba6878666c6f8c90a501e7f3eb2c79b9eea3012d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 17:35:47 +1100 Subject: [PATCH 12/23] Fixes pretty ugly issue of the Saving/Saved events not being raised when using CreateWithIdentity --- src/Umbraco.Core/Services/ContentService.cs | 20 +++++++++++++++++ src/Umbraco.Core/Services/MediaService.cs | 24 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 5cb6c5b974..c90c0b5029 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -176,12 +176,20 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) { content.WasCancelled = true; return content; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { @@ -191,6 +199,8 @@ namespace Umbraco.Core.Services uow.Commit(); } + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); Audit.Add(AuditTypes.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); @@ -216,12 +226,20 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) { content.WasCancelled = true; return content; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { @@ -231,6 +249,8 @@ namespace Umbraco.Core.Services uow.Commit(); } + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); Audit.Add(AuditTypes.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 3a864bd397..dfeb271936 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -122,12 +122,23 @@ namespace Umbraco.Core.Services { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parentId, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) { media.WasCancelled = true; return media; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + //TODO: Once we fix up the transaction logic, these write locks should be replaced with + // an outter transaction instead. using (new WriteLock(Locker)) { var uow = _uowProvider.GetUnitOfWork(); @@ -142,6 +153,8 @@ namespace Umbraco.Core.Services } } + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); Audit.Add(AuditTypes.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); @@ -166,12 +179,21 @@ namespace Umbraco.Core.Services { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parent, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) { media.WasCancelled = true; return media; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + using (new WriteLock(Locker)) { var uow = _uowProvider.GetUnitOfWork(); @@ -186,6 +208,8 @@ namespace Umbraco.Core.Services } } + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); Audit.Add(AuditTypes.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); From 4adc2562b1c3c248849aeb6391eec53db1497574 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 17:49:09 +1100 Subject: [PATCH 13/23] Fixes issue with ugolive dashboard trying to load in knockout from a bundle which causes the examine mgr to fail --- src/Umbraco.Web.UI/umbraco/plugins/uGoLive/Dashboard.ascx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/plugins/uGoLive/Dashboard.ascx b/src/Umbraco.Web.UI/umbraco/plugins/uGoLive/Dashboard.ascx index a5ee2d2ba5..2f55eda0ce 100644 --- a/src/Umbraco.Web.UI/umbraco/plugins/uGoLive/Dashboard.ascx +++ b/src/Umbraco.Web.UI/umbraco/plugins/uGoLive/Dashboard.ascx @@ -5,7 +5,6 @@ - From d0cc93d6a2daa6cefaba7aca50fad56a453bcf9b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 17:50:08 +1100 Subject: [PATCH 14/23] Fixes: U4-3937 Ensure media is indexed across all servers in LB environment with Distributed Cache calls This fix is for media indexes to be distributed properly and for unpublished content to be distributed properly, now to get members to do the same. --- src/Umbraco.Core/Models/ContentExtensions.cs | 14 + src/Umbraco.Core/Models/EntityExtensions.cs | 2 +- .../Cache/CacheRefresherEventHandler.cs | 81 ++++- src/Umbraco.Web/Cache/DistributedCache.cs | 1 + .../Cache/DistributedCacheExtensions.cs | 38 +- src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 36 +- .../Cache/UnpublishedPageCacheRefresher.cs | 33 ++ src/Umbraco.Web/Search/ExamineEvents.cs | 334 ++++++++++++------ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 9 files changed, 404 insertions(+), 136 deletions(-) create mode 100644 src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 8b3440d435..e89cb6e2af 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -25,6 +25,20 @@ namespace Umbraco.Core.Models { #region IContent + /// + /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) + /// + /// + /// + /// + /// This is helpful for determining if the published event will execute during the saved event for a content item. + /// + internal static bool JustPublished(this IContent entity) + { + var dirty = (IRememberBeingDirty)entity; + return dirty.WasPropertyDirty("Published") && entity.Published; + } + /// /// Determines if a new version should be created /// diff --git a/src/Umbraco.Core/Models/EntityExtensions.cs b/src/Umbraco.Core/Models/EntityExtensions.cs index f461c4007c..6daf99a58d 100644 --- a/src/Umbraco.Core/Models/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/EntityExtensions.cs @@ -23,5 +23,5 @@ namespace Umbraco.Core.Models var dirty = (IRememberBeingDirty)entity; return dirty.WasPropertyDirty("Id"); } - } + } } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index a9b01fb6e5..6f8e708219 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -131,19 +131,30 @@ namespace Umbraco.Web.Cache MediaService.Moving += MediaServiceMoving; MediaService.Trashing += MediaServiceTrashing; - ContentService.Created += ContentServiceCreated; + //Bind to content events - this is for unpublished content syncing across servers (primarily for examine) + + ContentService.Saved += ContentServiceSaved; + ContentService.Deleted += ContentServiceDeleted; ContentService.Copied += ContentServiceCopied; + //NOTE: we do not listen for the trashed event because there is no cache to update for content in that case since + // the unpublishing event handles that, and for examine with unpublished content indexes, we want to keep that data + // in the index, it's not until it's complete deleted that we want to remove it. } + + #region Content service event handlers /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. + /// Handles cache refreshgi for when content is copied /// /// /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) + /// + /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the + /// case then we need to clear all user permissions cache. + /// + static void ContentServiceCopied(IContentService sender, CopyEventArgs e) { //check if permissions have changed var permissionsChanged = ((Content)e.Copy).WasPropertyDirty("PermissionsChanged"); @@ -151,23 +162,63 @@ namespace Umbraco.Web.Cache { DistributedCache.Instance.RefreshAllUserPermissionsCache(); } + + //run the un-published cache refresher + DistributedCache.Instance.RefreshUnpublishedPageCache(e.Copy); } /// - /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. + /// Handles cache refreshing for when content is deleted (not unpublished) /// /// /// - static void ContentServiceCreated(IContentService sender, Core.Events.NewEventArgs e) + static void ContentServiceDeleted(IContentService sender, DeleteEventArgs e) { - //check if permissions have changed - var permissionsChanged = ((Content)e.Entity).WasPropertyDirty("PermissionsChanged"); - if (permissionsChanged) + DistributedCache.Instance.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); + } + + /// + /// Handles cache refreshing for when content is saved (not published) + /// + /// + /// + /// + /// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to + /// stay up-to-date for unpublished content. + /// + /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the + /// case then we need to clear all user permissions cache. + /// + static void ContentServiceSaved(IContentService sender, SaveEventArgs e) + { + var clearUserPermissions = false; + e.SavedEntities.ForEach(x => + { + //check if it is new + if (x.IsNewEntity()) + { + //check if permissions have changed + var permissionsChanged = ((Content)x).WasPropertyDirty("PermissionsChanged"); + if (permissionsChanged) + { + clearUserPermissions = true; + } + } + }); + + if (clearUserPermissions) { DistributedCache.Instance.RefreshAllUserPermissionsCache(); } - } + + //filter out the entities that have only been saved (not newly published) since + // newly published ones will be synced with the published page cache refresher + var unpublished = e.SavedEntities.Where(x => x.JustPublished() == false); + //run the un-published cache refresher + DistributedCache.Instance.RefreshUnpublishedPageCache(unpublished.ToArray()); + } + + #endregion #region ApplicationTree event handlers @@ -451,12 +502,12 @@ namespace Umbraco.Web.Cache InvalidateCacheForPermissionsChange(sender); } - void UserServiceSavedUser(IUserService sender, Core.Events.SaveEventArgs e) + static void UserServiceSavedUser(IUserService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserCache(x.Id)); } - void UserServiceDeletedUser(IUserService sender, Core.Events.DeleteEventArgs e) + static void UserServiceDeletedUser(IUserService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } @@ -547,7 +598,7 @@ namespace Umbraco.Web.Cache #region Media event handlers static void MediaServiceTrashing(IMediaService sender, Core.Events.MoveEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(e.Entity); + DistributedCache.Instance.RemoveMediaCache(false, e.Entity); } static void MediaServiceMoving(IMediaService sender, Core.Events.MoveEventArgs e) @@ -557,7 +608,7 @@ namespace Umbraco.Web.Cache static void MediaServiceDeleting(IMediaService sender, Core.Events.DeleteEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(e.DeletedEntities.ToArray()); + DistributedCache.Instance.RemoveMediaCache(true, e.DeletedEntities.ToArray()); } static void MediaServiceSaved(IMediaService sender, Core.Events.SaveEventArgs e) diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index b66dd2174a..8fa1dec3b3 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Cache public const string ApplicationCacheRefresherId = "B15F34A1-BC1D-4F8B-8369-3222728AB4C8"; public const string TemplateRefresherId = "DD12B6A0-14B9-46e8-8800-C154F74047C8"; public const string PageCacheRefresherId = "27AB3022-3DFA-47b6-9119-5945BC88FD66"; + public const string UnpublishedPageCacheRefresherId = "55698352-DFC5-4DBE-96BD-A4A0F6F77145"; public const string MemberCacheRefresherId = "E285DF34-ACDC-4226-AE32-C0CB5CF388DA"; public const string MemberGroupCacheRefresherId = "187F236B-BD21-4C85-8A7C-29FBA3D6C00C"; public const string MediaCacheRefresherId = "B29286DD-2D40-4DDB-B325-681226589FEC"; diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index faede0bffc..e218537ef2 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -123,8 +123,7 @@ namespace Umbraco.Web.Cache } #endregion - - + #region Data type cache /// /// Refreshes the cache amongst servers for a data type @@ -232,7 +231,28 @@ namespace Umbraco.Web.Cache public static void RemovePageCache(this DistributedCache dc, int documentId) { dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId); - } + } + + /// + /// invokes the unpublished page cache refresher + /// + /// + /// + public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Refresh(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + } + + /// + /// invokes the unpublished page cache refresher + /// + /// + /// + public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + } + #endregion #region Member cache @@ -291,7 +311,7 @@ namespace Umbraco.Web.Cache public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) { dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(media)); + MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); } /// @@ -304,6 +324,7 @@ namespace Umbraco.Web.Cache /// to clear all of the cache but the media item will be removed before the other servers can /// look it up. Only here for legacy purposes. /// + [Obsolete("Ensure to clear with other RemoveMediaCache overload")] public static void RemoveMediaCache(this DistributedCache dc, int mediaId) { dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); @@ -313,11 +334,14 @@ namespace Umbraco.Web.Cache /// Removes the cache amongst servers for media items /// /// + /// /// - public static void RemoveMediaCache(this DistributedCache dc, params IMedia[] media) + public static void RemoveMediaCache(this DistributedCache dc, bool isPermanentlyDeleted, params IMedia[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(media)); + dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), + MediaCacheRefresher.SerializeToJsonPayload( + isPermanentlyDeleted ? MediaCacheRefresher.OperationType.Deleted : MediaCacheRefresher.OperationType.Trashed, + media)); } #endregion diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 53abacc7a5..72a541b720 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Web.Script.Serialization; using Umbraco.Core; using Umbraco.Core.Cache; @@ -19,13 +20,13 @@ namespace Umbraco.Web.Cache public class MediaCacheRefresher : JsonCacheRefresherBase { #region Static helpers - + /// /// Converts the json to a JsonPayload object /// /// /// - private static JsonPayload[] DeserializeFromJsonPayload(string json) + internal static JsonPayload[] DeserializeFromJsonPayload(string json) { var serializer = new JavaScriptSerializer(); var jsonObject = serializer.Deserialize(json); @@ -35,12 +36,13 @@ namespace Umbraco.Web.Cache /// /// Creates the custom Json payload used to refresh cache amongst the servers /// + /// /// /// - internal static string SerializeToJsonPayload(params IMedia[] media) + internal static string SerializeToJsonPayload(OperationType operation, params IMedia[] media) { var serializer = new JavaScriptSerializer(); - var items = media.Select(FromMedia).ToArray(); + var items = media.Select(x => FromMedia(x, operation)).ToArray(); var json = serializer.Serialize(items); return json; } @@ -49,15 +51,17 @@ namespace Umbraco.Web.Cache /// Converts a macro to a jsonPayload object /// /// + /// /// - private static JsonPayload FromMedia(IMedia media) + internal static JsonPayload FromMedia(IMedia media, OperationType operation) { if (media == null) return null; var payload = new JsonPayload { Id = media.Id, - Path = media.Path + Path = media.Path, + Operation = operation }; return payload; } @@ -66,10 +70,18 @@ namespace Umbraco.Web.Cache #region Sub classes - private class JsonPayload + internal enum OperationType + { + Saved, + Trashed, + Deleted + } + + internal class JsonPayload { public string Path { get; set; } public int Id { get; set; } + public OperationType Operation { get; set; } } #endregion @@ -97,13 +109,15 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id))); + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), OperationType.Saved)); base.Refresh(id); } public override void Remove(int id) - { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id))); + { + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), + //NOTE: we'll just default to trashed for this one. + OperationType.Trashed)); base.Remove(id); } @@ -121,7 +135,7 @@ namespace Umbraco.Web.Cache string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString()) + if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs new file mode 100644 index 0000000000..4413a61e9b --- /dev/null +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -0,0 +1,33 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Cache +{ + /// + /// A cache refresher used for non-published content, this is primarily to notify Examine indexes to update + /// + public sealed class UnpublishedPageCacheRefresher : TypedCacheRefresherBase + { + protected override UnpublishedPageCacheRefresher Instance + { + get { return this; } + } + + public override Guid UniqueIdentifier + { + get { return new Guid(DistributedCache.UnpublishedPageCacheRefresherId); } + } + + public override string Name + { + get { return "Unpublished Page Refresher"; } + } + + //NOTE: There is no functionality for this cache refresher, it is here simply to emit events on each server for which examine + // binds to. We could put the Examine index functionality in here but we've kept it all in the ExamineEvents class so that all of + // the logic is in one place. In the future we may put the examine logic in a cache refresher instead (that would make sense) but we'd + // want to get this done before making more cache refreshers: + // http://issues.umbraco.org/issue/U4-2633 + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 102038a7c0..0d4cb9cdeb 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Security; using System.Xml; @@ -7,9 +8,13 @@ using Examine; using Examine.LuceneEngine; using Lucene.Net.Documents; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; using UmbracoExamine; using umbraco; using umbraco.BusinessLogic; @@ -50,20 +55,12 @@ namespace Umbraco.Web.Search if (registeredProviders == 0) return; - MediaService.Saved += MediaServiceSaved; - MediaService.Deleted += MediaServiceDeleted; - MediaService.Moved += MediaServiceMoved; - MediaService.Trashed += MediaServiceTrashed; - - ContentService.Saved += ContentServiceSaved; - ContentService.Deleted += ContentServiceDeleted; - ContentService.Moved += ContentServiceMoved; - ContentService.Trashed += ContentServiceTrashed; - - //These should only fire for providers that DONT have SupportUnpublishedContent set to true - content.AfterUpdateDocumentCache += ContentAfterUpdateDocumentCache; - content.AfterClearDocumentCache += ContentAfterClearDocumentCache; - + //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; + Member.AfterSave += MemberAfterSave; Member.AfterDelete += MemberAfterDelete; @@ -79,60 +76,192 @@ namespace Umbraco.Web.Search } } - [SecuritySafeCritical] - static void ContentServiceTrashed(IContentService sender, Core.Events.MoveEventArgs e) + /// + /// Handles index management for all media events - basically handling saving/copying/trashing/deleting + /// + /// + /// + static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) { - IndexConent(e.Entity); + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMedia(c1, c1.Trashed == false); + } + break; + case MessageType.RemoveById: + var c2 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c2 != null) + { + //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) + { + switch (payload.Operation) + { + case MediaCacheRefresher.OperationType.Saved: + var media = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media != null) + { + ReIndexForMedia(media, media.Trashed == false); + } + break; + case MediaCacheRefresher.OperationType.Trashed: + //keep if trashed for indexes supporting unpublished + DeleteIndexForEntity(payload.Id, true); + break; + case MediaCacheRefresher.OperationType.Deleted: + //permanently remove from all indexes + DeleteIndexForEntity(payload.Id, false); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + 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 + /// [SecuritySafeCritical] - static void MediaServiceTrashed(IMediaService sender, Core.Events.MoveEventArgs e) + static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) { - IndexMedia(e.Entity); + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForContent(c1, true); + } + 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) + { + // 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); + } + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IContent; + if (c3 != null) + { + 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 + /// [SecuritySafeCritical] - static void ContentServiceMoved(IContentService sender, Umbraco.Core.Events.MoveEventArgs e) + static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) { - IndexConent(e.Entity); - } + 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: - [SecuritySafeCritical] - static void ContentServiceDeleted(IContentService sender, Umbraco.Core.Events.DeleteEventArgs e) - { - e.DeletedEntities.ForEach( - content => - ExamineManager.Instance.DeleteFromIndex( - content.Id.ToString(), - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler))); - } + // This is triggered when the item is permanently deleted - [SecuritySafeCritical] - static void ContentServiceSaved(IContentService sender, Umbraco.Core.Events.SaveEventArgs e) - { - e.SavedEntities.ForEach(IndexConent); - } - - [SecuritySafeCritical] - static void MediaServiceMoved(IMediaService sender, Umbraco.Core.Events.MoveEventArgs e) - { - IndexMedia(e.Entity); - } - - [SecuritySafeCritical] - static void MediaServiceDeleted(IMediaService sender, Umbraco.Core.Events.DeleteEventArgs e) - { - e.DeletedEntities.ForEach( - media => - ExamineManager.Instance.DeleteFromIndex( - media.Id.ToString(), - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler))); - } - - [SecuritySafeCritical] - static void MediaServiceSaved(IMediaService sender, Umbraco.Core.Events.SaveEventArgs e) - { - e.SavedEntities.ForEach(IndexMedia); + var c4 = e.MessageObject as IContent; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } } [SecuritySafeCritical] @@ -156,39 +285,6 @@ namespace Umbraco.Web.Search .Where(x => x.EnableDefaultEventHandler)); } - /// - /// Only Update indexes for providers that dont SupportUnpublishedContent - /// - /// - /// - [SecuritySafeCritical] - private static void ContentAfterUpdateDocumentCache(Document sender, DocumentCacheEventArgs e) - { - //ensure that only the providers that have DONT unpublishing support enabled - //that are also flagged to listen - ExamineManager.Instance.ReIndexNode(ToXDocument(sender, true).Root, IndexTypes.Content, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => !x.SupportUnpublishedContent - && x.EnableDefaultEventHandler)); - } - - /// - /// Only update indexes for providers that don't SupportUnpublishedContnet - /// - /// - /// - [SecuritySafeCritical] - private static void ContentAfterClearDocumentCache(Document sender, DocumentCacheEventArgs e) - { - var nodeId = sender.Id.ToString(); - //ensure that only the providers that DONT have unpublishing support enabled - //that are also flagged to listen - ExamineManager.Instance.DeleteFromIndex(nodeId, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => !x.SupportUnpublishedContent - && 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 @@ -210,27 +306,61 @@ namespace Umbraco.Web.Search } } - - private static void IndexMedia(IMedia sender) + [SecuritySafeCritical] + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { ExamineManager.Instance.ReIndexNode( sender.ToXml(), "media", - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler)); + 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)); } - private static void IndexConent(IContent sender) + /// + /// 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. + /// + [SecuritySafeCritical] + 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.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 + /// + [SecuritySafeCritical] + private static void ReIndexForContent(IContent sender, bool isContentPublished) { - //only index this content if the indexer supports unpublished content. that is because the - // content.AfterUpdateDocumentCache will handle anything being published and will only index against indexers - // that only support published content. - // NOTE: The events for publishing have changed slightly from 6.0 to 6.1 and are streamlined in 6.1. Before - // this event would fire before publishing, then again after publishing. Now the save event fires once before - // publishing and that is all. - ExamineManager.Instance.ReIndexNode( sender.ToXml(), "content", ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.SupportUnpublishedContent && x.EnableDefaultEventHandler)); + + //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)); } /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 944d51536a..bff6ea8189 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -288,6 +288,7 @@ + From e7fb6e5bebed6031c38e8f9e7a8749a69e7736fc Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 18:07:02 +1100 Subject: [PATCH 15/23] Streamlines member and user services CreateMemberWithIdentity to be similar to media/content and not have a raise events flag. Also implements the CreateWithIdentity explicitly --- .../Services/IMembershipMemberService.cs | 5 ++- .../Services/IMembershipUserService.cs | 2 +- src/Umbraco.Core/Services/MemberService.cs | 22 ++++++------- src/Umbraco.Core/Services/UserService.cs | 16 ++++------ .../UmbracoServiceMembershipProviderTests.cs | 6 ++-- .../Services/UserServiceTests.cs | 32 +++++++++---------- 6 files changed, 37 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index a4522a95ff..c2183bb254 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Services public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService { IMember CreateMember(string username, string email, string password, string memberType); - IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType, bool raiseEvents = true); + IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType); } /// @@ -47,9 +47,8 @@ namespace Umbraco.Core.Services /// /// /// - /// /// - T CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true); + T CreateWithIdentity(string username, string email, string password, string memberTypeAlias); /// /// Gets the member by the provider key diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs index 57be495f47..2e4db90d3a 100644 --- a/src/Umbraco.Core/Services/IMembershipUserService.cs +++ b/src/Umbraco.Core/Services/IMembershipUserService.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Services public interface IMembershipUserService : IMembershipMemberService { - IUser CreateMemberWithIdentity(string username, string email, string password, IUserType userType, bool raiseEvents = true); + IUser CreateUserWithIdentity(string username, string email, string password, IUserType userType); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 6dba8cd184..fd6d190c9f 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -587,19 +587,16 @@ namespace Umbraco.Core.Services return member; } - public IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType, bool raiseEvents = true) + public IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType) { if (memberType == null) throw new ArgumentNullException("memberType"); var member = new Member(username, email.ToLower().Trim(), username, password, -1, memberType); - if (raiseEvents) + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) - { - member.WasCancelled = true; - return member; - } + member.WasCancelled = true; + return member; } var uow = _uowProvider.GetUnitOfWork(); @@ -613,12 +610,12 @@ namespace Umbraco.Core.Services CreateAndSaveMemberXml(xml, member.Id, uow.Database); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); return member; } - + /// /// Creates and persists a new Member /// @@ -626,9 +623,8 @@ namespace Umbraco.Core.Services /// /// /// - /// /// - public IMember CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true) + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string password, string memberTypeAlias) { var uow = _uowProvider.GetUnitOfWork(); IMemberType memberType; @@ -644,7 +640,7 @@ namespace Umbraco.Core.Services throw new ArgumentException(string.Format("No MemberType matching the passed in Alias: '{0}' was found", memberTypeAlias)); } - return CreateMemberWithIdentity(username, email, password, memberType, raiseEvents); + return CreateMemberWithIdentity(username, email, password, memberType); } /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index db0d20ed50..ded1f5bd35 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Services } } - public IUser CreateMemberWithIdentity(string username, string email, string password, IUserType userType, bool raiseEvents = true) + public IUser CreateUserWithIdentity(string username, string email, string password, IUserType userType) { if (userType == null) throw new ArgumentNullException("userType"); @@ -103,23 +103,19 @@ namespace Umbraco.Core.Services IsApproved = true }; - if (raiseEvents) - { - if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) - return user; - } + if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) + return user; repository.AddOrUpdate(user); uow.Commit(); - if (raiseEvents) - SavedUser.RaiseEvent(new SaveEventArgs(user, false), this); + SavedUser.RaiseEvent(new SaveEventArgs(user, false), this); return user; } } - public IUser CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true) + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string password, string memberTypeAlias) { var userType = GetUserTypeByAlias(memberTypeAlias); if (userType == null) @@ -127,7 +123,7 @@ namespace Umbraco.Core.Services throw new ArgumentException("The user type " + memberTypeAlias + " could not be resolved"); } - return CreateMemberWithIdentity(username, email, password, userType); + return CreateUserWithIdentity(username, email, password, userType); } public IUser GetById(int id) diff --git a/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs b/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs index 0053dc758e..64305d6b1c 100644 --- a/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs +++ b/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs @@ -94,7 +94,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); @@ -125,7 +125,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 224d3d335b..22d3a9d516 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Services // Arrange var userService = ServiceContext.UserService; var userType = userService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "123456", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "123456", userType); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Services // Arrange var userService = ServiceContext.UserService; var userType = userService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "123456", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "123456", userType); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -97,7 +97,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); ServiceContext.UserService.Delete(user, true); var deleted = ServiceContext.UserService.GetUserById(user.Id); @@ -111,7 +111,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); ServiceContext.UserService.Delete(user); var deleted = ServiceContext.UserService.GetUserById(user.Id); @@ -125,7 +125,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsTrue(ServiceContext.UserService.Exists("JohnDoe")); Assert.IsFalse(ServiceContext.UserService.Exists("notFound")); @@ -136,7 +136,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByEmail(user.Email)); Assert.IsNull(ServiceContext.UserService.GetByEmail("do@not.find")); @@ -147,7 +147,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByUsername(user.Username)); Assert.IsNull(ServiceContext.UserService.GetByUsername("notFound")); @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("mydomain\\JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("mydomain\\JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByUsername(user.Username)); Assert.IsNull(ServiceContext.UserService.GetByUsername("notFound")); @@ -169,7 +169,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetUserById(user.Id)); Assert.IsNull(ServiceContext.UserService.GetUserById(9876)); @@ -351,7 +351,7 @@ namespace Umbraco.Tests.Services var userType = userService.GetUserTypeByAlias("admin"); // Act - var membershipUser = userService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var membershipUser = userService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); // Assert Assert.That(membershipUser.HasIdentity, Is.True); @@ -374,7 +374,7 @@ namespace Umbraco.Tests.Services var hash = new HMACSHA1(); hash.Key = Encoding.Unicode.GetBytes(password); var encodedPassword = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password))); - var membershipUser = userService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", encodedPassword, userType); + var membershipUser = userService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", encodedPassword, userType); // Assert Assert.That(membershipUser.HasIdentity, Is.True); @@ -390,8 +390,8 @@ namespace Umbraco.Tests.Services { var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user1 = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); - var user2 = ServiceContext.UserService.CreateMemberWithIdentity("test2", "test2@test.com", "test2", userType); + var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); + var user2 = ServiceContext.UserService.CreateUserWithIdentity("test2", "test2@test.com", "test2", userType); //adds some allowed sections user1.AddAllowedSection("test"); @@ -415,7 +415,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act @@ -432,7 +432,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user = (IUser)ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var user = (IUser)ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act @@ -449,7 +449,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var originalUser = (User)ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var originalUser = (User)ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act From 72c5853fb1cac98b9505028b0f708795367effb8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 18:25:38 +1100 Subject: [PATCH 16/23] Completes: U4-3937 Ensure media is indexed across all servers in LB environment with Distributed Cache calls for members --- src/Umbraco.Web.UI/config/trees.config | 15 +-- .../config/umbracoSettings.config | 4 +- .../Cache/CacheRefresherEventHandler.cs | 26 ++--- .../Cache/DistributedCacheExtensions.cs | 23 ++++ src/Umbraco.Web/Cache/MemberCacheRefresher.cs | 14 ++- src/Umbraco.Web/Search/ExamineEvents.cs | 101 ++++++++++++------ 6 files changed, 117 insertions(+), 66 deletions(-) diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 78e9dfc0db..6834440077 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -1,25 +1,21 @@  - - - - - + + - @@ -28,24 +24,19 @@ - - + - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index feb1c5e82f..2662dd0ef6 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -224,7 +224,7 @@ - + 0 @@ -233,7 +233,7 @@ - umb1.dev + localhost diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 6f8e708219..92751092a0 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -596,22 +596,22 @@ namespace Umbraco.Web.Cache #endregion #region Media event handlers - static void MediaServiceTrashing(IMediaService sender, Core.Events.MoveEventArgs e) + static void MediaServiceTrashing(IMediaService sender, MoveEventArgs e) { DistributedCache.Instance.RemoveMediaCache(false, e.Entity); } - static void MediaServiceMoving(IMediaService sender, Core.Events.MoveEventArgs e) + static void MediaServiceMoving(IMediaService sender, MoveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.Entity); } - static void MediaServiceDeleting(IMediaService sender, Core.Events.DeleteEventArgs e) + static void MediaServiceDeleting(IMediaService sender, DeleteEventArgs e) { DistributedCache.Instance.RemoveMediaCache(true, e.DeletedEntities.ToArray()); } - static void MediaServiceSaved(IMediaService sender, Core.Events.SaveEventArgs e) + static void MediaServiceSaved(IMediaService sender, SaveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.SavedEntities.ToArray()); } @@ -619,27 +619,21 @@ namespace Umbraco.Web.Cache #region Member event handlers - static void MemberServiceDeleted(IMemberService sender, Core.Events.DeleteEventArgs e) + static void MemberServiceDeleted(IMemberService sender, DeleteEventArgs e) { - foreach (var m in e.DeletedEntities.ToArray()) - { - DistributedCache.Instance.RemoveMemberCache(m.Id); - } + DistributedCache.Instance.RemoveMemberCache(e.DeletedEntities.ToArray()); } - static void MemberServiceSaved(IMemberService sender, Core.Events.SaveEventArgs e) + static void MemberServiceSaved(IMemberService sender, SaveEventArgs e) { - foreach (var m in e.SavedEntities.ToArray()) - { - DistributedCache.Instance.RefreshMemberCache(m.Id); - } + DistributedCache.Instance.RefreshMemberCache(e.SavedEntities.ToArray()); } #endregion #region Member group event handlers - static void MemberGroupService_Deleted(IMemberGroupService sender, Core.Events.DeleteEventArgs e) + static void MemberGroupService_Deleted(IMemberGroupService sender, DeleteEventArgs e) { foreach (var m in e.DeletedEntities.ToArray()) { @@ -647,7 +641,7 @@ namespace Umbraco.Web.Cache } } - static void MemberGroupService_Saved(IMemberGroupService sender, Core.Events.SaveEventArgs e) + static void MemberGroupService_Saved(IMemberGroupService sender, SaveEventArgs e) { foreach (var m in e.SavedEntities.ToArray()) { diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index e218537ef2..5b3d5ca02f 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -256,11 +256,33 @@ namespace Umbraco.Web.Cache #endregion #region Member cache + + /// + /// Refreshes the cache among servers for a member + /// + /// + /// + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + } + + /// + /// Removes the cache among servers for a member + /// + /// + /// + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + } + /// /// Refreshes the cache among servers for a member /// /// /// + [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] public static void RefreshMemberCache(this DistributedCache dc, int memberId) { dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), memberId); @@ -271,6 +293,7 @@ namespace Umbraco.Web.Cache /// /// /// + [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] public static void RemoveMemberCache(this DistributedCache dc, int memberId) { dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), memberId); diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 2b740e826d..5267602402 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Cache /// /// This is not intended to be used directly in your code and it should be sealed but due to legacy code we cannot seal it. /// - public class MemberCacheRefresher : CacheRefresherBase + public class MemberCacheRefresher : TypedCacheRefresherBase { protected override MemberCacheRefresher Instance @@ -44,6 +44,18 @@ namespace Umbraco.Web.Cache base.Remove(id); } + public override void Refresh(IMember instance) + { + ClearCache(instance.Id); + base.Remove(instance); + } + + public override void Remove(IMember instance) + { + ClearCache(instance.Id); + base.Remove(instance); + } + private void ClearCache(int id) { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 0d4cb9cdeb..c04920e289 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -60,10 +60,8 @@ namespace Umbraco.Web.Search CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; - Member.AfterSave += MemberAfterSave; - Member.AfterDelete += MemberAfterDelete; - var contentIndexer = ExamineManager.Instance.IndexProviderCollection["InternalIndexer"] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -76,11 +74,55 @@ namespace Umbraco.Web.Search } } - /// + [SecuritySafeCritical] + static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) + { + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMember(c1); + } + 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 IMember; + if (c3 != null) + { + ReIndexForMember(c3); + } + break; + case MessageType.RemoveByInstance: + + // This is triggered when the item is permanently deleted + + var c4 = e.MessageObject as IMember; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } + } + + /// /// Handles index management for all media events - basically handling saving/copying/trashing/deleting /// /// /// + [SecuritySafeCritical] static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) { switch (e.MessageType) @@ -265,24 +307,13 @@ namespace Umbraco.Web.Search } [SecuritySafeCritical] - private static void MemberAfterSave(Member sender, SaveEventArgs e) + private static void ReIndexForMember(IMember member) { - //ensure that only the providers are flagged to listen execute - var xml = ExamineXmlExtensions.ToXElement(sender.ToXml(new System.Xml.XmlDocument(), false)); - var providers = ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler); - ExamineManager.Instance.ReIndexNode(xml, IndexTypes.Member, providers); - } - - [SecuritySafeCritical] - private static void MemberAfterDelete(Member sender, DeleteEventArgs e) - { - var nodeId = sender.Id.ToString(); - - //ensure that only the providers are flagged to listen execute - ExamineManager.Instance.DeleteFromIndex(nodeId, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler)); + ExamineManager.Instance.ReIndexNode( + member.ToXml(), IndexTypes.Member, + ExamineManager.Instance.IndexProviderCollection.OfType() + //ensure that only the providers are flagged to listen execute + .Where(x => x.EnableDefaultEventHandler)); } /// @@ -310,14 +341,14 @@ namespace Umbraco.Web.Search private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { ExamineManager.Instance.ReIndexNode( - sender.ToXml(), "media", + sender.ToXml(), 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 + //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)); + .Where(x => isMediaPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); } /// @@ -351,17 +382,17 @@ namespace Umbraco.Web.Search /// [SecuritySafeCritical] private static void ReIndexForContent(IContent sender, bool isContentPublished) - { - ExamineManager.Instance.ReIndexNode( - sender.ToXml(), "content", - ExamineManager.Instance.IndexProviderCollection.OfType() + { + ExamineManager.Instance.ReIndexNode( + sender.ToXml(), 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 + //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)); - } + .Where(x => isContentPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); + } /// /// Converts a content node to XDocument From 2c0f6f975cef674650df532d497d9b2db9846b68 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 6 Mar 2014 08:42:41 +0100 Subject: [PATCH 17/23] Bugfix UmbracoEntity ctor test --- src/Umbraco.Tests/Models/UmbracoEntityTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs index 2be59c7b07..a24b5f1c35 100644 --- a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs +++ b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; using Umbraco.Core.Models; namespace Umbraco.Tests.Models @@ -10,10 +11,10 @@ namespace Umbraco.Tests.Models public void UmbracoEntity_Can_Be_Initialized_From_Dynamic() { var boolIsTrue = true; - var intIsTrue = 1; + ulong ulongIsTrue = 1; // because MySql might return ulong var trashedWithBool = new UmbracoEntity((dynamic)boolIsTrue); - var trashedWithInt = new UmbracoEntity((dynamic)intIsTrue); + var trashedWithInt = new UmbracoEntity((dynamic)ulongIsTrue); Assert.IsTrue(trashedWithBool.Trashed); Assert.IsTrue(trashedWithInt.Trashed); From ea4be14f0c8d1a9668441898ab95c608ce0758bc Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 6 Mar 2014 09:10:14 +0100 Subject: [PATCH 18/23] Fix 026d040 - keep Standalone stuff internal --- src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs | 2 +- src/Umbraco.Web/Standalone/WriteableConfigSystem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs index 8c323902c0..63dd2ff252 100644 --- a/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs +++ b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Umbraco.Web.Standalone { - public static class PowershellAssemblyResolver + internal static class PowershellAssemblyResolver { private static readonly Dictionary Assemblies; private static readonly object Locko = new object(); diff --git a/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs index 86d187c393..2438256cce 100644 --- a/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs +++ b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Standalone // see http://stackoverflow.com/questions/15653621/how-to-update-add-modify-delete-keys-in-appsettings-section-of-web-config-at-r // see http://www.codeproject.com/Articles/69364/Override-Configuration-Manager - public sealed class WriteableConfigSystem : IInternalConfigSystem + internal sealed class WriteableConfigSystem : IInternalConfigSystem { private static readonly ReaderWriterLockSlim RwLock = new ReaderWriterLockSlim(); private static WriteableConfigSystem _installed; From 7f8c5261364d2e3d49c1d981a75431df6c15b4e1 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 6 Mar 2014 09:14:05 +0100 Subject: [PATCH 19/23] Fix TryConvertTo handling of number decimal separator --- src/Umbraco.Core/ObjectExtensions.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index d3b1184940..65ae278b48 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -237,12 +237,14 @@ namespace Umbraco.Core else if (destinationType == typeof(Double)) { Double value; - return Double.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Double.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Single)) { Single value; - return Single.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Single.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Char)) { @@ -311,7 +313,8 @@ namespace Umbraco.Core else if (destinationType == typeof(Decimal)) { Decimal value; - return Decimal.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Decimal.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Version)) { @@ -323,6 +326,14 @@ namespace Umbraco.Core return null; // we can't decide... } + private readonly static char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; + + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) { //TODO: Localise this exception From 01d04fde371f9a8b732cc58ba4ec697d3ffc8e76 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 19:29:09 +1100 Subject: [PATCH 20/23] Fixes: U4-4060 Rollback feature does not show differences --- src/umbraco.cms/businesslogic/CMSNode.cs | 25 +++++++++++ src/umbraco.cms/businesslogic/Content.cs | 44 ++++++++++++++----- src/umbraco.cms/businesslogic/web/Document.cs | 3 +- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index 4052d89cc4..5b4381e13d 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -377,6 +377,19 @@ namespace umbraco.cms.businesslogic setupNode(); } + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! this allows + /// inheritors to set default data before setupNode() is called. + /// + /// + /// + internal CMSNode(int id, object[] ctorArgs) + { + _id = id; + PreSetupNode(ctorArgs); + } + /// /// Initializes a new instance of the class. /// @@ -1016,6 +1029,18 @@ order by level,sortOrder"; _entity.Name = txt; } + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! + /// + /// + internal virtual void PreSetupNode(params object[] ctorArgs) + { + //if people want to override then awesome but then we call setupNode so they need to ensure + // to call base.PreSetupNode + setupNode(); + } + /// /// Sets up the internal data of the CMSNode, used by the various constructors /// diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index f2e52a1287..7dd0c264ae 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -39,13 +39,18 @@ namespace umbraco.cms.businesslogic private bool _versionDateInitialized; private string _contentTypeIcon; private ContentType _contentType; - private Properties m_LoadedProperties = null; + private Properties _loadedProperties = null; protected internal IContentBase ContentBase; #endregion #region Constructors - + + protected internal Content(int id, Guid version) + : base(id, new object[] { version }) + { + } + public Content(int id) : base(id) { } protected Content(int id, bool noSetup) : base(id, noSetup) { } @@ -223,7 +228,7 @@ namespace umbraco.cms.businesslogic get { EnsureProperties(); - return m_LoadedProperties; + return _loadedProperties; } } @@ -236,7 +241,7 @@ namespace umbraco.cms.businesslogic get { EnsureProperties(); - return m_LoadedProperties.ToArray(); + return _loadedProperties.ToArray(); } } @@ -288,7 +293,7 @@ namespace umbraco.cms.businesslogic { EnsureProperties(); - return m_LoadedProperties.SingleOrDefault(x => x.PropertyType.Alias == alias); + return _loadedProperties.SingleOrDefault(x => x.PropertyType.Alias == alias); } /// @@ -300,7 +305,7 @@ namespace umbraco.cms.businesslogic { EnsureProperties(); - return m_LoadedProperties.SingleOrDefault(x => x.PropertyType.Id == pt.Id); + return _loadedProperties.SingleOrDefault(x => x.PropertyType.Id == pt.Id); } /// @@ -462,6 +467,21 @@ namespace umbraco.cms.businesslogic #region Protected Methods + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! + /// + /// + internal override void PreSetupNode(params object[] ctorArgs) + { + //we know that there is one ctor arg and it is a GUID since we are only calling the base + // ctor with this overload for one purpose. + var version = (Guid) ctorArgs[0]; + _version = version; + + base.PreSetupNode(ctorArgs); + } + /// /// Sets up the ContentType property for this content item and sets the addition content properties manually. /// If the ContentType property is not already set, then this will get the ContentType from Cache. @@ -616,7 +636,7 @@ namespace umbraco.cms.businesslogic /// private void ClearLoadedProperties() { - m_LoadedProperties = null; + _loadedProperties = null; } /// @@ -624,7 +644,7 @@ namespace umbraco.cms.businesslogic /// private void EnsureProperties() { - if (m_LoadedProperties == null) + if (_loadedProperties == null) { InitializeProperties(); } @@ -643,13 +663,13 @@ namespace umbraco.cms.businesslogic /// private void InitializeProperties() { - m_LoadedProperties = new Properties(); + _loadedProperties = new Properties(); if (ContentBase != null) { //NOTE: we will not load any properties where HasIdentity = false - this is because if properties are // added to the property collection that aren't persisted we'll get ysods - m_LoadedProperties.AddRange(ContentBase.Properties.Where(x => x.HasIdentity).Select(x => new Property(x))); + _loadedProperties.AddRange(ContentBase.Properties.Where(x => x.HasIdentity).Select(x => new Property(x))); return; } @@ -658,7 +678,7 @@ namespace umbraco.cms.businesslogic //Create anonymous typed list with 2 props, Id and PropertyTypeId of type Int. //This will still be an empty list since the props list is empty. - var propData = m_LoadedProperties.Select(x => new { Id = 0, PropertyTypeId = 0 }).ToList(); + var propData = _loadedProperties.Select(x => new { Id = 0, PropertyTypeId = 0 }).ToList(); string sql = @"select id, propertyTypeId from cmsPropertyData where versionId=@versionId"; @@ -697,7 +717,7 @@ namespace umbraco.cms.businesslogic continue; //this remains from old code... not sure why we would do this? } - m_LoadedProperties.Add(p); + _loadedProperties.Add(p); } } diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index b95a8402f7..51ecb20967 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -55,9 +55,8 @@ namespace umbraco.cms.businesslogic.web /// The id of the document /// The version of the document public Document(int id, Guid Version) - : base(id) + : base(id, Version) { - this.Version = Version; } /// From 0fda528b9488868bbc079883eb5d4bd7bfea2b5b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 19:47:23 +1100 Subject: [PATCH 21/23] Fixes: U4-1550 Publish notifications on a load balanced environment --- .../LegacyActionHandlerEventHandler.cs | 43 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco.presentation/content.cs | 16 +------ 3 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs diff --git a/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs b/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs new file mode 100644 index 0000000000..4ab8b25849 --- /dev/null +++ b/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using umbraco.BusinessLogic.Actions; +using umbraco.cms.businesslogic.web; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Strategies +{ + + /// + /// This is used to trigger the legacy ActionHandlers based on events + /// + public sealed class LegacyActionHandlerEventHandler : ApplicationEventHandler + { + //NOTE: this is to fix this currently: http://issues.umbraco.org/issue/U4-1550 + + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + ContentService.Published += ContentService_Published; + ContentService.UnPublished += ContentService_UnPublished; + } + + static void ContentService_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + { + e.PublishedEntities.ForEach(x => + global::umbraco.BusinessLogic.Actions.Action.RunActionHandlers( + new Document(x), ActionUnPublish.Instance)); + } + + static void ContentService_Published(IPublishingStrategy sender, PublishEventArgs e) + { + e.PublishedEntities.ForEach(x => + global::umbraco.BusinessLogic.Actions.Action.RunActionHandlers( + new Document(x), ActionPublish.Instance)); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 86c7437ba3..11d70c41b8 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -401,6 +401,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 9270e57cc0..ab611bda87 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -521,9 +521,7 @@ namespace umbraco var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(cachedFieldKeyStart); - - Action.RunActionHandlers(d, ActionPublish.Instance); - + FireAfterUpdateDocumentCache(d, e); } } @@ -532,6 +530,7 @@ namespace umbraco /// 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 @@ -550,11 +549,6 @@ namespace umbraco XmlContentInternal = xmlContentCopy; ClearContextCache(); } - - foreach (Document d in Documents) - { - Action.RunActionHandlers(d, ActionPublish.Instance); - } } /// @@ -635,12 +629,6 @@ namespace umbraco } } - if (x != null) - { - // Run Handler - Action.RunActionHandlers(doc, ActionUnPublish.Instance); - } - //SD: changed to fire event BEFORE running the sitemap!! argh. FireAfterClearDocumentCache(doc, e); From 068bca84b7ef884fe022be8b16ed0b8c3cfffbaf Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 20:05:13 +1100 Subject: [PATCH 22/23] Fixes: U4-156 Drag / drop support --- .../controls/ContentTypeControlNew.ascx | 24 +++++++++++++++---- .../umbraco_client/ui/default.css | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx index 7fc9bbf44c..b803f94494 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx +++ b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx @@ -105,10 +105,26 @@ }); - jQuery(document).ready(function () { + $(document).ready(function () { + + $("table.tabs-table tr.propertyContent input.sort-order").keydown(function(e) { + // Allow: backspace, delete, tab, escape, enter and . + if ($.inArray(e.keyCode, [46, 8, 9, 27, 13, 190]) !== -1 || + // Allow: Ctrl+A + (e.keyCode == 65 && e.ctrlKey === true) || + // Allow: home, end, left, right + (e.keyCode >= 35 && e.keyCode <= 39)) { + // let it happen, don't do anything + return; + } + // Ensure that it is a number and stop the keypress + if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) { + e.preventDefault(); + } + }); // Make each tr of the tabs table sortable (prevent dragging of header row, and set up a callback for when row dragged) - jQuery("table.tabs-table tbody").sortable({ + $("table.tabs-table tbody").sortable({ containment: 'parent', cancel: '.propertyHeader, input', tolerance: 'pointer', @@ -119,8 +135,8 @@ // Fired after row dragged; go through each tr and save position to the hidden sort order field function saveOrder() { - jQuery("table.tabs-table tbody tr.propertyContent").each(function (index) { - jQuery("input.sort-order", this).val(index + 1); + $("table.tabs-table tbody tr.propertyContent").each(function (index) { + $("input.sort-order", this).val(index + 1); }); } diff --git a/src/Umbraco.Web.UI/umbraco_client/ui/default.css b/src/Umbraco.Web.UI/umbraco_client/ui/default.css index ecc1163290..576bd0d0ce 100644 --- a/src/Umbraco.Web.UI/umbraco_client/ui/default.css +++ b/src/Umbraco.Web.UI/umbraco_client/ui/default.css @@ -743,7 +743,12 @@ table.tabs-table tr.propertyContent td } table.tabs-table input[type=text] { width: 200px; } table.tabs-table input[type=submit] { text-align: right; } -table.tabs-table tr.propertyContent input.sort-order {display: none;} + +table.tabs-table tr.propertyContent input.sort-order { + width: 20px; + background-color: lightgray; + padding: 0px 5px 0px 5px; +} li.no-properties-on-tab {background: none; background-color: #fff; cursor: default; } From d61ab97be30841a37aee6e86858e3e67b1e72968 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Mar 2014 20:12:05 +1100 Subject: [PATCH 23/23] Fixes: U4-4217 Ensure that the cmsTask data is cleared for any entity that uses umbracoNode table --- src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs | 1 + src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs | 1 + src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs | 1 + src/Umbraco.Core/Persistence/Repositories/UserRepository.cs | 2 ++ 4 files changed, 5 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index d9dd6405fe..517c004e0f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -132,6 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 241b5ad799..47f202e34e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -124,6 +124,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 5b46e73658..a99cc47aa5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -167,6 +167,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 252756bdf4..8fac8d8c11 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -131,6 +131,8 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE userId = @Id", + "DELETE FROM cmsTask WHERE parentUserId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE userId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", "DELETE FROM umbracoUserLogins WHERE userId = @Id",