diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 0cc47263a6..bab1269932 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -14,6 +14,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Publishing; using Umbraco.Core.Macros; using Umbraco.Core.Services; +using Umbraco.Core.Sync; using MigrationsVersionFourNineZero = Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourNineZero; namespace Umbraco.Core @@ -165,6 +166,16 @@ namespace Umbraco.Core /// protected virtual void InitializeResolvers() { + //by default we'll use the standard configuration based sync + ServerRegistrarResolver.Current = new ServerRegistrarResolver( + new ConfigServerRegistrar()); + + //by default (outside of the web) we'll use the default server messenger without + //supplying a username/password, this will automatically disable distributed calls + // .. we'll override this in the WebBootManager + ServerMessengerResolver.Current = new ServerMessengerResolver( + new DefaultServerMessenger()); + RepositoryResolver.Current = new RepositoryResolver( new RepositoryFactory()); diff --git a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs index 90704648d1..63f32c41c5 100644 --- a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs @@ -9,25 +9,6 @@ namespace Umbraco.Core.Publishing public abstract class BasePublishingStrategy : IPublishingStrategy { - internal abstract Attempt PublishInternal(IContent content, int userId); - - /// - /// Publishes a list of content items - /// - /// - /// - /// - /// By default this is set to true which means that it will publish any content item in the list that is completely unpublished and - /// not visible on the front-end. If set to false, this will only publish content that is live on the front-end but has new versions - /// that have yet to be published. - /// - /// If true this will validate each content item before trying to publish it, if validation fails it will not be published. - /// - internal abstract IEnumerable> PublishWithChildrenInternal( - IEnumerable content, int userId, bool includeUnpublishedDocuments = true, bool validateContent = false); - - internal abstract IEnumerable> UnPublishInternal(IEnumerable content, int userId); - public abstract bool Publish(IContent content, int userId); public abstract bool PublishWithChildren(IEnumerable content, int userId); public abstract bool UnPublish(IContent content, int userId); diff --git a/src/Umbraco.Core/Publishing/PublishingStrategy.cs b/src/Umbraco.Core/Publishing/PublishingStrategy.cs index 4a8cc5a63d..5219604afa 100644 --- a/src/Umbraco.Core/Publishing/PublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/PublishingStrategy.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Publishing /// /// to publish /// Id of the User issueing the publish operation - internal override Attempt PublishInternal(IContent content, int userId) + internal Attempt PublishInternal(IContent content, int userId) { if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content), this)) { @@ -85,8 +85,7 @@ namespace Umbraco.Core.Publishing /// By default this is set to true which means that it will publish any content item in the list that is completely unpublished and /// not visible on the front-end. If set to false, this will only publish content that is live on the front-end but has new versions /// that have yet to be published. - /// - /// If true this will validate each content item before trying to publish it, if validation fails it will not be published. + /// /// /// /// This method becomes complex once we start to be able to cancel events or stop publishing a content item in any way because if a @@ -99,8 +98,8 @@ namespace Umbraco.Core.Publishing /// level and so on. If we detect that the above rule applies when the document publishing is cancelled we'll add it to the list of /// parentsIdsCancelled so that it's children don't get published. /// - internal override IEnumerable> PublishWithChildrenInternal( - IEnumerable content, int userId, bool includeUnpublishedDocuments = true, bool validateContent = false) + internal IEnumerable> PublishWithChildrenInternal( + IEnumerable content, int userId, bool includeUnpublishedDocuments = true) { var statuses = new List>(); @@ -163,7 +162,7 @@ namespace Umbraco.Core.Publishing } //Check if the content is valid if the flag is set to check - if (validateContent && !item.IsValid()) + if (!item.IsValid()) { LogHelper.Info( string.Format("Content '{0}' with Id '{1}' will not be published because some of it's content is not passing validation rules.", @@ -322,7 +321,7 @@ namespace Umbraco.Core.Publishing /// An enumerable list of /// Id of the User issueing the unpublish operation /// A list of publish statuses - internal override IEnumerable> UnPublishInternal(IEnumerable content, int userId) + internal IEnumerable> UnPublishInternal(IEnumerable content, int userId) { var result = new List>(); diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index eec0346f4d..0fdb4b06c6 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Services public class ContentService : IContentService { private readonly IDatabaseUnitOfWorkProvider _uowProvider; - private readonly BasePublishingStrategy _publishingStrategy; + private readonly IPublishingStrategy _publishingStrategy; private readonly RepositoryFactory _repositoryFactory; public ContentService() @@ -43,23 +43,16 @@ namespace Umbraco.Core.Services : this(provider, repositoryFactory, new PublishingStrategy()) { } - [Obsolete("This contructor is no longer supported, use the other constructor accepting a BasePublishginStrategy object instead")] public ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IPublishingStrategy publishingStrategy) { - _uowProvider = provider; - _publishingStrategy = publishingStrategy as BasePublishingStrategy; - if (_publishingStrategy == null) - throw new InvalidOperationException("publishingStrategy must be an instance of " + typeof(BasePublishingStrategy).Name); + if (provider == null) throw new ArgumentNullException("provider"); + if (repositoryFactory == null) throw new ArgumentNullException("repositoryFactory"); + if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); + _uowProvider = provider; + _publishingStrategy = publishingStrategy; _repositoryFactory = repositoryFactory; } - public ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, BasePublishingStrategy publishingStrategy) - { - _uowProvider = provider; - _publishingStrategy = publishingStrategy; - _repositoryFactory = repositoryFactory; - } - /// /// Creates an object using the alias of the /// that this Content is based on. @@ -477,7 +470,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public bool RePublishAll(int userId = 0) { - return RePublishAllDo(false, userId); + return RePublishAllDo(userId); } /// @@ -488,7 +481,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public bool Publish(IContent content, int userId = 0) { - var result = SaveAndPublishDo(content, false, userId); + var result = SaveAndPublishDo(content, userId); return result.Success; } @@ -500,7 +493,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public bool PublishWithChildren(IContent content, int userId = 0) { - var result = PublishWithChildrenDo(content, false, userId); + var result = PublishWithChildrenDo(content, userId); //This used to just return false only when the parent content failed, otherwise would always return true so we'll // do the same thing for the moment @@ -527,7 +520,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { - var result = SaveAndPublishDo(content, false, userId, raiseEvents); + var result = SaveAndPublishDo(content, userId, raiseEvents); return result.Success; } @@ -1063,67 +1056,41 @@ namespace Umbraco.Core.Services } #region Internal Methods - /// - /// Internal method to Re-Publishes all Content for legacy purposes. - /// - /// Optional Id of the User issueing the publishing - /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will not update the cache. - /// True if publishing succeeded, otherwise False - internal bool RePublishAll(bool omitCacheRefresh = true, int userId = 0) - { - return RePublishAllDo(omitCacheRefresh, userId); - } - + /// /// Internal method that Publishes a single object for legacy purposes. /// /// The to publish - /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False - internal Attempt Publish(IContent content, bool omitCacheRefresh = true, int userId = 0) + internal Attempt PublishInternal(IContent content, int userId = 0) { - return SaveAndPublishDo(content, omitCacheRefresh, userId); + return SaveAndPublishDo(content, userId); } /// /// Internal method that Publishes a object and all its children for legacy purposes. /// /// The to publish along with its children - /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published - /// If true this will validate the content before publishing /// True if publishing succeeded, otherwise False - internal IEnumerable> PublishWithChildren( - IContent content, bool omitCacheRefresh = true, int userId = 0, bool includeUnpublished = false, bool validateContent = false) + internal IEnumerable> PublishWithChildrenInternal( + IContent content, int userId = 0, bool includeUnpublished = false) { - return PublishWithChildrenDo(content, omitCacheRefresh, userId, includeUnpublished, validateContent); - } - - /// - /// Internal method that UnPublishes a single object for legacy purposes. - /// - /// The to publish - /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will not update the cache. - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - internal bool UnPublish(IContent content, bool omitCacheRefresh = true, int userId = 0) - { - return UnPublishDo(content, omitCacheRefresh, userId); + return PublishWithChildrenDo(content, userId, includeUnpublished); } /// /// Saves and Publishes a single object /// /// The to save and publish - /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False - internal Attempt SaveAndPublish(IContent content, bool omitCacheRefresh = true, int userId = 0, bool raiseEvents = true) + internal Attempt SaveAndPublishInternal(IContent content, int userId = 0, bool raiseEvents = true) { - return SaveAndPublishDo(content, omitCacheRefresh, userId, raiseEvents); + return SaveAndPublishDo(content, userId, raiseEvents); } /// @@ -1150,9 +1117,8 @@ namespace Umbraco.Core.Services /// Re-Publishes all Content /// /// Optional Id of the User issueing the publishing - /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False - private bool RePublishAllDo(bool omitCacheRefresh = false, int userId = 0) + private bool RePublishAllDo(int userId = 0) { var list = new List(); var updated = new List(); @@ -1198,9 +1164,8 @@ namespace Umbraco.Core.Services : Convert.ToInt32(uow.Database.Insert(poco)); } } - //Updating content to published state is finished, so we fire event through PublishingStrategy to have cache updated - if (omitCacheRefresh == false) - _publishingStrategy.PublishingFinalized(updated, true); + + _publishingStrategy.PublishingFinalized(updated, true); } Audit.Add(AuditTypes.Publish, "RePublish All performed by user", userId, -1); @@ -1212,17 +1177,15 @@ namespace Umbraco.Core.Services /// Publishes a object and all its children /// /// The to publish along with its children - /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// Optional Id of the User issueing the publishing - /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published - /// If set to true will ensure the content is valid before publishing + /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published /// /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that /// are to be published. /// private IEnumerable> PublishWithChildrenDo( - IContent content, bool omitCacheRefresh = false, int userId = 0, bool includeUnpublished = false, bool validateContent = false) + IContent content, int userId = 0, bool includeUnpublished = false) { if (content == null) throw new ArgumentNullException("content"); @@ -1257,8 +1220,10 @@ namespace Umbraco.Core.Services list.Add(content); //include parent item list.AddRange(GetDescendants(content)); + var internalStrategy = (PublishingStrategy)_publishingStrategy; + //Publish and then update the database with new status - var publishedOutcome = _publishingStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished, validateContent).ToArray(); + var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) @@ -1288,8 +1253,7 @@ namespace Umbraco.Core.Services } } //Save xml to db and call following method to fire event: - if (omitCacheRefresh == false) - _publishingStrategy.PublishingFinalized(updated, false); + _publishingStrategy.PublishingFinalized(updated, false); Audit.Add(AuditTypes.Publish, "Publish with Children performed by user", userId, content.Id); @@ -1334,11 +1298,10 @@ namespace Umbraco.Core.Services /// Saves and Publishes a single object /// /// The to save and publish - /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False - private Attempt SaveAndPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0, bool raiseEvents = true) + private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) { if(raiseEvents) { @@ -1372,8 +1335,9 @@ namespace Umbraco.Core.Services publishStatus.StatusType = PublishStatusType.FailedContentInvalid; } + var internalStrategy = (PublishingStrategy) _publishingStrategy; //Publish and then update the database with new status - var publishResult = _publishingStrategy.PublishInternal(content, userId); + var publishResult = internalStrategy.PublishInternal(content, userId); //set our publish status to the publish result publishStatus.StatusType = publishResult.Result.StatusType; @@ -1430,13 +1394,13 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(content, false), this); //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published && omitCacheRefresh == false) + if (published) { _publishingStrategy.PublishingFinalized(content); } //We need to check if children and their publish state to ensure that we 'republish' content that was previously published - if (published && omitCacheRefresh == false && previouslyPublished == false && HasChildren(content.Id)) + if (published && previouslyPublished == false && HasChildren(content.Id)) { var descendants = GetPublishedDescendants(content); diff --git a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs new file mode 100644 index 0000000000..8080ef05fa --- /dev/null +++ b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; +using Umbraco.Core.Configuration; + +namespace Umbraco.Core.Sync +{ + /// + /// A registrar that uses the legacy xml configuration in umbracoSettings to get a list of defined server nodes + /// + internal class ConfigServerRegistrar : IServerRegistrar + { + private readonly XmlNode _xmlServers; + + public ConfigServerRegistrar() + : this(UmbracoSettings.DistributionServers) + { + + } + + internal ConfigServerRegistrar(XmlNode xmlServers) + { + _xmlServers = xmlServers; + } + + private List _addresses; + + public IEnumerable Registrations + { + get + { + if (_addresses == null) + { + _addresses = new List(); + var nodes = _xmlServers.SelectNodes("./server"); + foreach (XmlNode n in nodes) + { + _addresses.Add(new ConfigServerRegistration(n)); + } + } + return _addresses; + } + } + } +} diff --git a/src/Umbraco.Core/Sync/ConfigServerRegistration.cs b/src/Umbraco.Core/Sync/ConfigServerRegistration.cs new file mode 100644 index 0000000000..c8865bd4a2 --- /dev/null +++ b/src/Umbraco.Core/Sync/ConfigServerRegistration.cs @@ -0,0 +1,29 @@ +using System.Xml; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Sync +{ + /// + /// A server registration based on the legacy umbraco xml configuration in umbracoSettings + /// + internal class ConfigServerRegistration : IServerRegistration + { + + public ConfigServerRegistration(XmlNode n) + { + var webServicesUrl = IOHelper.ResolveUrl(SystemDirectories.WebServices); + + var protocol = GlobalSettings.UseSSL ? "https" : "http"; + if (n.Attributes.GetNamedItem("forceProtocol") != null && !string.IsNullOrEmpty(n.Attributes.GetNamedItem("forceProtocol").Value)) + protocol = n.Attributes.GetNamedItem("forceProtocol").Value; + var domain = XmlHelper.GetNodeValue(n); + if (n.Attributes.GetNamedItem("forcePortnumber") != null && !string.IsNullOrEmpty(n.Attributes.GetNamedItem("forcePortnumber").Value)) + domain += string.Format(":{0}", n.Attributes.GetNamedItem("forcePortnumber").Value); + ServerAddress = string.Format("{0}://{1}{2}/cacheRefresher.asmx", protocol, domain, webServicesUrl); + } + + public string ServerAddress { get; private set; } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs new file mode 100644 index 0000000000..2bc9e18467 --- /dev/null +++ b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using System.Web.Script.Serialization; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// The default server messenger that uses web services to keep servers in sync + /// + internal class DefaultServerMessenger : IServerMessenger + { + private readonly string _login; + private readonly string _password; + private readonly bool _useDistributedCalls; + + /// + /// Without a username/password all distribuion will be disabled + /// + internal DefaultServerMessenger() + { + _useDistributedCalls = false; + } + + /// + /// Distribution will be enabled based on the umbraco config setting. + /// + /// + /// + internal DefaultServerMessenger(string login, string password) + { + _useDistributedCalls = UmbracoSettings.UseDistributedCalls; + _login = login; + _password = password; + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher,Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + //copy local + var idGetter = getNumericId; + + MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, + x => idGetter(x), + instances); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + //copy local + var idGetter = getGuidId; + + MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, + x => idGetter(x), + instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + //copy local + var idGetter = getNumericId; + + MessageSeversForManyObjects(servers, refresher, MessageType.RemoveById, + x => idGetter(x), + instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + MessageSeversForManyIds(servers, refresher, MessageType.RemoveById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + MessageSeversForManyIds(servers, refresher, MessageType.RefreshById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + MessageSeversForManyIds(servers, refresher, MessageType.RefreshById, guidIds.Cast()); + } + + public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) + { + MessageSeversForManyIds(servers, refresher, MessageType.RefreshAll, Enumerable.Empty().ToArray()); + } + + private void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, Func getId, IEnumerable instances) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + var stronglyTypedRefresher = refresher as ICacheRefresher; + + foreach (var instance in instances) + { + //if we are not, then just invoke the call on the cache refresher + switch (dispatchType) + { + case MessageType.RefreshAll: + refresher.RefreshAll(); + break; + case MessageType.RefreshById: + if (stronglyTypedRefresher != null) + { + stronglyTypedRefresher.Refresh(instance); + } + else + { + var id = getId(instance); + if (id is int) + { + refresher.Refresh((int)id); + } + else if (id is Guid) + { + refresher.Refresh((Guid)id); + } + else + { + throw new InvalidOperationException("The id must be either an int or a Guid"); + } + } + break; + case MessageType.RemoveById: + if (stronglyTypedRefresher != null) + { + stronglyTypedRefresher.Remove(instance); + } + else + { + var id = getId(instance); + refresher.Refresh((int)id); + } + break; + } + } + } + + private void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, IEnumerable ids) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + //if it is a refresh all we'll do it here since ids will be null or empty + if (dispatchType == MessageType.RefreshAll) + { + refresher.RefreshAll(); + } + else + { + foreach (var id in ids) + { + //if we are not, then just invoke the call on the cache refresher + switch (dispatchType) + { + case MessageType.RefreshById: + if (id is int) + { + refresher.Refresh((int)id); + } + else if (id is Guid) + { + refresher.Refresh((Guid)id); + } + else + { + throw new InvalidOperationException("The id must be either an int or a Guid"); + } + + break; + case MessageType.RemoveById: + refresher.Remove((int)id); + break; + } + } + } + + + + } + + private void MessageSeversForManyObjects( + IEnumerable servers, + ICacheRefresher refresher, + MessageType dispatchType, + Func getId, + IEnumerable instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + //Now, check if we are using Distrubuted calls. If there are no servers in the list then we + // can definitely not distribute. + if (!_useDistributedCalls || !servers.Any()) + { + //if we are not, then just invoke the call on the cache refresher + InvokeMethodOnRefresherInstance(refresher, dispatchType, getId, instances); + return; + } + + //if we are distributing calls then we'll need to do it by id + MessageSeversForManyIds(servers, refresher, dispatchType, instances.Select(getId)); + } + + private void MessageSeversForManyIds( + IEnumerable servers, + ICacheRefresher refresher, + MessageType dispatchType, + IEnumerable ids) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + Type arrayType = null; + foreach (var id in ids) + { + if (!(id is int) && (!(id is Guid))) + throw new ArgumentException("The id must be either an int or a Guid"); + if (arrayType == null) + arrayType = id.GetType(); + if (arrayType != id.GetType()) + throw new ArgumentException("The array must contain the same type of " + arrayType); + } + + //Now, check if we are using Distrubuted calls. If there are no servers in the list then we + // can definitely not distribute. + if (!_useDistributedCalls || !servers.Any()) + { + //if we are not, then just invoke the call on the cache refresher + InvokeMethodOnRefresherInstance(refresher, dispatchType, ids); + return; + } + + //We are using distributed calls, so lets make them... + try + { + using (var cacheRefresher = new ServerSyncWebServiceClient()) + { + var asyncResultsList = new List(); + + LogStartDispatch(); + + // Go through each configured node submitting a request asynchronously + foreach (var n in servers) + { + //set the server address + cacheRefresher.Url = n.ServerAddress; + + // Add the returned WaitHandle to the list for later checking + switch (dispatchType) + { + case MessageType.RefreshAll: + asyncResultsList.Add( + cacheRefresher.BeginRefreshAll( + refresher.UniqueIdentifier, _login, _password, null, null)); + break; + case MessageType.RefreshById: + if (arrayType == typeof(int)) + { + var serializer = new JavaScriptSerializer(); + var jsonIds = serializer.Serialize(ids.Cast().ToArray()); + //we support bulk loading of Integers + var result = cacheRefresher.BeginRefreshByIds(refresher.UniqueIdentifier, jsonIds, _login, _password, null, null); + asyncResultsList.Add(result); + } + else + { + //we don't currently support bulk loading of GUIDs (not even sure if we have any Guid ICacheRefreshers) + //so we'll just iterate + asyncResultsList.AddRange( + ids.Select(i => cacheRefresher.BeginRefreshByGuid( + refresher.UniqueIdentifier, (Guid) i, _login, _password, null, null))); + } + + break; + case MessageType.RemoveById: + //we don't currently support bulk removing so we'll iterate + asyncResultsList.AddRange( + ids.Select(i => cacheRefresher.BeginRemoveById( + refresher.UniqueIdentifier, (int)i, _login, _password, null, null))); + break; + } + } + + List waitHandlesList; + var asyncResults = GetAsyncResults(asyncResultsList, out waitHandlesList); + + var errorCount = 0; + + // Once for each WaitHandle that we have, wait for a response and log it + // We're previously submitted all these requests effectively in parallel and will now retrieve responses on a FIFO basis + foreach (var t in asyncResults) + { + var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); + + try + { + // Find out if the call succeeded + switch (dispatchType) + { + case MessageType.RefreshAll: + cacheRefresher.EndRefreshAll(t); + break; + case MessageType.RefreshById: + if (arrayType == typeof(int)) + { + cacheRefresher.EndRefreshById(t); + } + else + { + cacheRefresher.EndRefreshByGuid(t); + } + break; + case MessageType.RemoveById: + cacheRefresher.EndRemoveById(t); + break; + } + } + catch (WebException ex) + { + LogDispatchNodeError(ex); + + errorCount++; + } + catch (Exception ex) + { + LogDispatchNodeError(ex); + + errorCount++; + } + } + + LogDispatchBatchResult(errorCount); + } + } + catch (Exception ee) + { + LogDispatchBatchError(ee); + } + } + + private IEnumerable GetAsyncResults(List asyncResultsList, out List waitHandlesList) + { + var asyncResults = asyncResultsList.ToArray(); + waitHandlesList = new List(); + foreach (var asyncResult in asyncResults) + { + waitHandlesList.Add(asyncResult.AsyncWaitHandle); + } + return asyncResults; + } + + private void LogDispatchBatchError(Exception ee) + { + LogHelper.Error("Error refreshing distributed list", ee); + } + + private void LogDispatchBatchResult(int errorCount) + { + LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); + } + + private void LogDispatchNodeError(Exception ex) + { + LogHelper.Error("Error refreshing a node in the distributed list", ex); + } + + private void LogDispatchNodeError(WebException ex) + { + string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; + LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); + } + + private void LogStartDispatch() + { + LogHelper.Info("Submitting calls to distributed servers"); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ICacheRefresher.cs b/src/Umbraco.Core/Sync/ICacheRefresher.cs new file mode 100644 index 0000000000..eeb93184f8 --- /dev/null +++ b/src/Umbraco.Core/Sync/ICacheRefresher.cs @@ -0,0 +1,18 @@ +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// Strongly type cache refresher that is able to refresh cache of real instances of objects as well as IDs + /// + /// + /// + /// This is much better for performance when we're not running in a load balanced environment so we can refresh the cache + /// against a already resolved object instead of looking the object back up by id. + /// + interface ICacheRefresher : ICacheRefresher + { + void Refresh(T instance); + void Remove(T instance); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs new file mode 100644 index 0000000000..f2112ea22d --- /dev/null +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// Defines a server messenger for server sync and distrubuted cache + /// + internal interface IServerMessenger + { + /// + /// Performs a sync against all instance objects + /// + /// + /// The servers to sync against + /// + /// A delegate to return the Id for each instance to be used to sync to other servers + /// + void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); + + /// + /// Performs a sync against all instance objects + /// + /// + /// The servers to sync against + /// + /// A delegate to return the Id for each instance to be used to sync to other servers + /// + void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances); + + /// + /// Removes the cache for the specified items + /// + /// + /// + /// + /// A delegate to return the Id for each instance to be used to sync to other servers + /// + void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); + + /// + /// Removes the cache for the specified items + /// + /// + /// + /// + void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); + + /// + /// Performs a sync against all Ids + /// + /// The servers to sync against + /// + /// + void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); + + /// + /// Performs a sync against all Ids + /// + /// The servers to sync against + /// + /// + void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds); + + /// + /// Performs entire cache refresh for a specified refresher + /// + /// + /// + void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher); + } + +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerRegistrar.cs b/src/Umbraco.Core/Sync/IServerRegistrar.cs new file mode 100644 index 0000000000..b0f6cba20f --- /dev/null +++ b/src/Umbraco.Core/Sync/IServerRegistrar.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Sync +{ + /// + /// An interface to expose a list of server registrations for server syncing + /// + internal interface IServerRegistrar + { + IEnumerable Registrations { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerRegistration.cs b/src/Umbraco.Core/Sync/IServerRegistration.cs new file mode 100644 index 0000000000..a5d80180b8 --- /dev/null +++ b/src/Umbraco.Core/Sync/IServerRegistration.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Sync +{ + /// + /// An interface exposing a server address to use for server syncing + /// + internal interface IServerRegistration + { + string ServerAddress { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/MessageType.cs b/src/Umbraco.Core/Sync/MessageType.cs new file mode 100644 index 0000000000..c4c66e99e2 --- /dev/null +++ b/src/Umbraco.Core/Sync/MessageType.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Sync +{ + /// + /// The message type to be used for syncing across servers + /// + internal enum MessageType + { + RefreshAll, + RefreshById, + RemoveById + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerMessengerResolver.cs b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs new file mode 100644 index 0000000000..b24001c877 --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Core.Sync +{ + /// + /// A resolver to return the currently registered IServerMessenger object + /// + internal class ServerMessengerResolver : SingleObjectResolverBase + { + internal ServerMessengerResolver(IServerMessenger factory) + : base(factory) + { + } + + /// + /// Can be used at runtime to set a custom IServerMessenger at app startup + /// + /// + public void SetServerMessenger(IServerMessenger serverMessenger) + { + Value = serverMessenger; + } + + public IServerMessenger Messenger + { + get { return Value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs new file mode 100644 index 0000000000..3da7747c33 --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs @@ -0,0 +1,31 @@ +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Core.Sync +{ + /// + /// The resolver to return the currently registered IServerRegistrar object + /// + internal class ServerRegistrarResolver : SingleObjectResolverBase + { + + internal ServerRegistrarResolver(IServerRegistrar factory) + : base(factory) + { + } + + /// + /// Can be used at runtime to set a custom IServerRegistrar at app startup + /// + /// + public void SetServerRegistrar(IServerRegistrar serverRegistrar) + { + Value = serverRegistrar; + } + + public IServerRegistrar Registrar + { + get { return Value; } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/CacheRefresherClient.cs b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs similarity index 74% rename from src/Umbraco.Web/Cache/CacheRefresherClient.cs rename to src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs index a382b3fd26..ca2add64d4 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherClient.cs +++ b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs @@ -1,19 +1,19 @@ -using Umbraco.Core.IO; +using System.Collections.Generic; +using System.Web.Services; +using Umbraco.Core.IO; -namespace Umbraco.Web.Cache +namespace Umbraco.Core.Sync { /// /// The client Soap service for making distrubuted cache calls between servers /// - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name = "CacheRefresherSoap", Namespace = "http://umbraco.org/webservices/")] - internal class CacheRefresherClient : System.Web.Services.Protocols.SoapHttpClientProtocol + [WebServiceBinding(Name = "CacheRefresherSoap", Namespace = "http://umbraco.org/webservices/")] + internal class ServerSyncWebServiceClient : System.Web.Services.Protocols.SoapHttpClientProtocol { /// - public CacheRefresherClient() + public ServerSyncWebServiceClient() { // only set the url if the httpcontext is present, else it's set by the cache dispatcher methods (when using distributed calls) if (System.Web.HttpContext.Current != null) @@ -74,7 +74,11 @@ namespace Umbraco.Web.Cache } /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://umbraco.org/webservices/RefreshById", RequestNamespace = "http://umbraco.org/webservices/", ResponseNamespace = "http://umbraco.org/webservices/", Use = System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] + [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://umbraco.org/webservices/RefreshById", + RequestNamespace = "http://umbraco.org/webservices/", + ResponseNamespace = "http://umbraco.org/webservices/", + Use = System.Web.Services.Description.SoapBindingUse.Literal, + ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public void RefreshById(System.Guid uniqueIdentifier, int Id, string Login, string Password) { this.Invoke("RefreshById", new object[] { @@ -100,6 +104,40 @@ namespace Umbraco.Web.Cache this.EndInvoke(asyncResult); } + + /// + [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://umbraco.org/webservices/RefreshByIds", + RequestNamespace = "http://umbraco.org/webservices/", + ResponseNamespace = "http://umbraco.org/webservices/", + Use = System.Web.Services.Description.SoapBindingUse.Literal, + ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] + public void RefreshByIds(System.Guid uniqueIdentifier, string jsonIds, string Login, string Password) + { + this.Invoke("RefreshByIds", new object[] { + uniqueIdentifier, + jsonIds, + Login, + Password}); + } + + /// + public System.IAsyncResult BeginRefreshByIds(System.Guid uniqueIdentifier, string jsonIds, string Login, string Password, System.AsyncCallback callback, object asyncState) + { + return this.BeginInvoke("RefreshByIds", new object[] { + uniqueIdentifier, + jsonIds, + Login, + Password}, callback, asyncState); + } + + /// + public void EndRefreshByIds(System.IAsyncResult asyncResult) + { + this.EndInvoke(asyncResult); + } + + + /// [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://umbraco.org/webservices/RemoveById", RequestNamespace = "http://umbraco.org/webservices/", ResponseNamespace = "http://umbraco.org/webservices/", Use = System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public void RemoveById(System.Guid uniqueIdentifier, int Id, string Login, string Password) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4db747c5c1..8fca0351d7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -61,6 +61,7 @@ + True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.Helpers.dll @@ -73,6 +74,7 @@ True ..\packages\Microsoft.AspNet.Razor.2.0.20715.0\lib\net40\System.Web.Razor.dll + True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.dll @@ -667,6 +669,19 @@ + + + + Component + + + + + + + + + diff --git a/src/Umbraco.Tests/ContentStores/PublishMediaStoreTests.cs b/src/Umbraco.Tests/ContentStores/PublishMediaStoreTests.cs index cf06b08faa..a60c52d966 100644 --- a/src/Umbraco.Tests/ContentStores/PublishMediaStoreTests.cs +++ b/src/Umbraco.Tests/ContentStores/PublishMediaStoreTests.cs @@ -34,7 +34,6 @@ namespace Umbraco.Tests.ContentStores PublishedMediaTests.DoTearDown(); } - [Ignore] [Test] public void Get_Root_Docs() { @@ -53,7 +52,6 @@ namespace Umbraco.Tests.ContentStores } - [Ignore] [Test] public void Get_Item_Without_Examine() { diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs index 4b2f351e0f..5b7475d6b1 100644 --- a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs +++ b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs @@ -93,7 +93,7 @@ namespace Umbraco.Tests.Publishing //publish root and nodes at it's children level var listToPublish = ServiceContext.ContentService.GetDescendants(_homePage.Id).Concat(new[] { _homePage }); - var result = strategy.PublishWithChildrenInternal(listToPublish, 0, true, true); + var result = strategy.PublishWithChildrenInternal(listToPublish, 0, true); Assert.AreEqual(listToPublish.Count() - 2, result.Count(x => x.Success)); Assert.IsTrue(result.Where(x => x.Success).Select(x => x.Result.ContentItem.Id) diff --git a/src/Umbraco.Tests/Sync/ConfigServerRegistrarTests.cs b/src/Umbraco.Tests/Sync/ConfigServerRegistrarTests.cs new file mode 100644 index 0000000000..c537c19072 --- /dev/null +++ b/src/Umbraco.Tests/Sync/ConfigServerRegistrarTests.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Text; +using System.Xml; +using NUnit.Framework; +using Umbraco.Core.Sync; +using Umbraco.Tests.PartialTrust; + +namespace Umbraco.Tests.Sync +{ + [TestFixture] + public class ConfigServerRegistrarTests + { + + [TestCase("127.0.0.1", "http://127.0.0.1")] + [TestCase("www.somedomain.com", "https://www.somedomain.com")] + [TestCase("another.domain.com.au", "http://another.domain.com.au:888")] + [TestCase("another.domain.com.au", "https://another.domain.com.au:999")] + public void Ensure_Correct_Format(string xml, string match) + { + var xDoc = new XmlDocument(); + xDoc.LoadXml(xml); + var xNode = xDoc.FirstChild; + var cReg = new ConfigServerRegistration(xNode); + + Assert.AreEqual(match + "/umbraco/webservices/cacheRefresher.asmx", cReg.ServerAddress); + } + + [Test] + public void Ensure_Parses_Config_Block() + { + var xDoc = new XmlDocument(); + xDoc.LoadXml(@" + 127.0.0.1 + www.somedomain.com + another.domain.com.au + another.domain.com.au +"); + var xNode = xDoc.FirstChild; + var cReg = new ConfigServerRegistrar(xNode); + + Assert.AreEqual(4, cReg.Registrations.Count()); + } + + } +} diff --git a/src/Umbraco.Tests/Sync/DistributedCacheTests.cs b/src/Umbraco.Tests/Sync/DistributedCacheTests.cs new file mode 100644 index 0000000000..847e947599 --- /dev/null +++ b/src/Umbraco.Tests/Sync/DistributedCacheTests.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; +using umbraco.interfaces; + +namespace Umbraco.Tests.Sync +{ + /// + /// Ensures that calls to DistributedCache methods carry through to the IServerMessenger correctly + /// + [TestFixture] + public class DistributedCacheTests + { + [SetUp] + public void Setup() + { + ServerRegistrarResolver.Current = new ServerRegistrarResolver( + new TestServerRegistrar()); + ServerMessengerResolver.Current = new ServerMessengerResolver( + new TestServerMessenger()); + CacheRefreshersResolver.Current = new CacheRefreshersResolver(() => new[] { typeof(TestCacheRefresher) }); + Resolution.Freeze(); + } + + [TearDown] + public void Teardown() + { + ServerRegistrarResolver.Reset(); + ServerMessengerResolver.Reset(); + CacheRefreshersResolver.Reset(); + } + + [Test] + public void RefreshIntId() + { + for (var i = 0; i < 10; i++) + { + DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); + } + Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); + } + + [Test] + public void RefreshIntIdFromObject() + { + for (var i = 0; i < 10; i++) + { + DistributedCache.Instance.Refresh( + Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), + x => x.Id, + new TestObjectWithId{Id = i}); + } + Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); + } + + [Test] + public void RefreshGuidId() + { + for (var i = 0; i < 11; i++) + { + DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), Guid.NewGuid()); + } + Assert.AreEqual(11, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).GuidIdsRefreshed.Count); + } + + [Test] + public void RemoveIds() + { + for (var i = 0; i < 12; i++) + { + DistributedCache.Instance.Remove(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); + } + Assert.AreEqual(12, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRemoved.Count); + } + + [Test] + public void FullRefreshes() + { + for (var i = 0; i < 13; i++) + { + DistributedCache.Instance.RefreshAll(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73")); + } + Assert.AreEqual(13, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).CountOfFullRefreshes); + } + + #region internal test classes + + internal class TestObjectWithId + { + public int Id { get; set; } + } + + internal class TestCacheRefresher : ICacheRefresher + { + public Guid UniqueIdentifier + { + get { return Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"); } + } + public string Name + { + get { return "Test"; } + } + public void RefreshAll() + { + + } + + public void Refresh(int id) + { + + } + + public void Remove(int id) + { + + } + + public void Refresh(Guid id) + { + + } + } + + internal class TestServerMessenger : IServerMessenger + { + //used for tests + public List IntIdsRefreshed = new List(); + public List GuidIdsRefreshed = new List(); + public List IntIdsRemoved = new List(); + public int CountOfFullRefreshes = 0; + + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + IntIdsRefreshed.AddRange(instances.Select(getNumericId)); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) + { + GuidIdsRefreshed.AddRange(instances.Select(getGuidId)); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + IntIdsRemoved.AddRange(instances.Select(getNumericId)); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + IntIdsRemoved.AddRange(numericIds); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + IntIdsRefreshed.AddRange(numericIds); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) + { + GuidIdsRefreshed.AddRange(guidIds); + } + + public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) + { + CountOfFullRefreshes++; + } + } + + internal class TestServerRegistrar : IServerRegistrar + { + public IEnumerable Registrations + { + get + { + return new List() + { + new TestServerRegistration("localhost") + }; + } + } + } + + public class TestServerRegistration : IServerRegistration + { + public TestServerRegistration(string address) + { + ServerAddress = address; + } + public string ServerAddress { get; private set; } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 3e172e260f..d95f7f07a3 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -78,7 +78,7 @@ namespace Umbraco.Tests.TestHelpers if (RequiresDbSetup) { //Create the umbraco database and its base data - DatabaseContext.Database.CreateDatabaseSchema(); + DatabaseContext.Database.CreateDatabaseSchema(false); } //called so that inheritors can do stuff before freezing. diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 96438c48b2..8ca2e35506 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -261,6 +261,8 @@ + + diff --git a/src/Umbraco.Web.UI/config/metablogConfig.config b/src/Umbraco.Web.UI/config/metablogConfig.config index adf71d17d0..83cc2c1aa1 100644 --- a/src/Umbraco.Web.UI/config/metablogConfig.config +++ b/src/Umbraco.Web.UI/config/metablogConfig.config @@ -5,7 +5,7 @@ 0 1080 False - Base + Home diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 1fafe2f61b..e1e02de465 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -150,14 +150,16 @@ - - - - 0 - - - - + + + + 0 + + + + umblb1.dev + localhost + diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs b/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs index 4dd6feb26a..7e09d0b425 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs +++ b/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs @@ -11,6 +11,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs protected string PageName { get; private set; } protected int DocumentId { get; private set; } + protected string DocumentPath { get; private set; } protected override void OnInit(EventArgs e) { @@ -30,6 +31,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs DocumentId = doc.Id; PageName = doc.Name; + DocumentPath = doc.Path; } } diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx index 2a0a65ff51..e93221ba25 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx @@ -23,7 +23,8 @@ $(document).ready(function () { Umbraco.Dialogs.PublishDialog.getInstance().init({ restServiceLocation: "<%= Url.GetBulkPublishServicePath() %>", - documentId: <%= DocumentId %> + documentId: <%= DocumentId %>, + documentPath: '<%= DocumentPath %>' }); }); })(jQuery); diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/PublishDialog.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/PublishDialog.js index f361ec39a1..d034a07a4a 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/PublishDialog.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/PublishDialog.js @@ -56,7 +56,10 @@ else { self._koViewModel.resultMessage(msgs[0]); } - + + //sync the tree + UmbClientMgr.mainTree().setActiveTreeType('content'); + UmbClientMgr.mainTree().syncTree(self._opts.documentPath, true); }); } }; diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index f5f4d35b14..9dfd262b6e 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -1,6 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Services; using umbraco; +using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.macro; using umbraco.cms.businesslogic.member; @@ -18,6 +19,11 @@ namespace Umbraco.Web.Cache { if (UmbracoSettings.UmbracoLibraryCacheDuration <= 0) return; + //Bind to user events + + User.Saving += UserSaving; + User.Deleting += UserDeleting; + //Bind to template events Template.AfterSave += TemplateAfterSave; @@ -44,6 +50,16 @@ namespace Umbraco.Web.Cache MediaService.Trashing += MediaServiceTrashing; } + static void UserDeleting(User sender, System.EventArgs e) + { + DistributedCache.Instance.RemoveUserCache(sender.Id); + } + + static void UserSaving(User sender, System.EventArgs e) + { + DistributedCache.Instance.RefreshUserCache(sender.Id); + } + /// /// Removes cache for template /// @@ -86,28 +102,22 @@ namespace Umbraco.Web.Cache static void MediaServiceTrashing(IMediaService sender, Core.Events.MoveEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(e.Entity.Id); + DistributedCache.Instance.RemoveMediaCache(e.Entity); } static void MediaServiceMoving(IMediaService sender, Core.Events.MoveEventArgs e) { - DistributedCache.Instance.RefreshMediaCache(e.Entity.Id); + DistributedCache.Instance.RefreshMediaCache(e.Entity); } static void MediaServiceDeleting(IMediaService sender, Core.Events.DeleteEventArgs e) { - foreach (var item in e.DeletedEntities) - { - DistributedCache.Instance.RemoveMediaCache(item.Id); - } + DistributedCache.Instance.RemoveMediaCache(e.DeletedEntities.ToArray()); } static void MediaServiceSaved(IMediaService sender, Core.Events.SaveEventArgs e) { - foreach (var item in e.SavedEntities) - { - DistributedCache.Instance.RefreshMediaCache(item.Id); - } + DistributedCache.Instance.RefreshMediaCache(e.SavedEntities.ToArray()); } static void MemberBeforeDelete(Member sender, DeleteEventArgs e) diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index 9a44b115a0..6793454894 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -9,23 +9,20 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; using umbraco.BusinessLogic; using umbraco.interfaces; namespace Umbraco.Web.Cache { /// - /// DistrubutedCacheDispatcher is used to handle Umbraco's load balancing. + /// DistributedCache is used to invalidate cache throughout the application which also takes in to account load balancing environments automatically /// /// /// Distributing calls to all registered load balanced servers, ensuring that content are synced and cached on all servers. /// Dispatcher is exendable, so 3rd party services can easily be integrated into the workflow, using the interfaces.ICacheRefresher interface. /// /// Dispatcher can refresh/remove content, templates and macros. - /// Load balanced servers are registered in umbracoSettings.config. - /// - /// UPDATE 2010 02 - Alex Norcliffe - Refactored Dispatcher to support parallel dispatch threads, and preventing failure of whole dispatch - /// if one node fails. Still needs more work to get it to Enterprise level though but this is for 4.1 /// public class DistributedCache { @@ -37,22 +34,17 @@ namespace Umbraco.Web.Cache public const string MemberCacheRefresherId = "E285DF34-ACDC-4226-AE32-C0CB5CF388DA"; public const string MediaCacheRefresherId = "B29286DD-2D40-4DDB-B325-681226589FEC"; public const string MacroCacheRefresherId = "7B1E683C-5F34-43dd-803D-9699EA1E98CA"; + public const string UserCacheRefresherId = "E057AF6D-2EE6-41F4-8045-3694010F0AA6"; #endregion - private readonly string _login; - private readonly string _password; - private readonly string _webServicesUrl; private static readonly DistributedCache InstanceObject = new DistributedCache(); /// /// Constructor /// private DistributedCache() - { - _login = User.GetUser(UmbracoSettings.DistributedCallUser).LoginName; - _password = User.GetUser(UmbracoSettings.DistributedCallUser).GetPassword(); - _webServicesUrl = IOHelper.ResolveUrl(SystemDirectories.WebServices); + { } /// @@ -67,6 +59,26 @@ namespace Umbraco.Web.Cache } } + /// + /// Sends a request to all registered load-balanced servers to refresh node with the specified Id + /// using the specified ICacheRefresher with the guid factoryGuid. + /// + /// + /// + /// The callback method to retreive the ID from an instance + /// The instances containing Ids + /// + /// This method is much better for performance because it does not need to re-lookup an object instance + /// + public void Refresh(Guid factoryGuid, Func getNumericId, params T[] instances) + { + ServerMessengerResolver.Current.Messenger.PerformRefresh( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid), + getNumericId, + instances); + } + /// /// Sends a request to all registered load-balanced servers to refresh node with the specified Id /// using the specified ICacheRefresher with the guid factoryGuid. @@ -75,7 +87,10 @@ namespace Umbraco.Web.Cache /// The id of the node. public void Refresh(Guid factoryGuid, int id) { - InvokeDispatchMethod(DispatchType.RefreshByNumericId, factoryGuid, id, Guid.Empty); + ServerMessengerResolver.Current.Messenger.PerformRefresh( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid), + id); } /// @@ -86,7 +101,10 @@ namespace Umbraco.Web.Cache /// The guid of the node. public void Refresh(Guid factoryGuid, Guid id) { - InvokeDispatchMethod(DispatchType.RefreshByGuid, factoryGuid, 0, id); + ServerMessengerResolver.Current.Messenger.PerformRefresh( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid), + id); } /// @@ -96,7 +114,9 @@ namespace Umbraco.Web.Cache /// The unique identifier. public void RefreshAll(Guid factoryGuid) { - InvokeDispatchMethod(DispatchType.RefreshAll, factoryGuid, 0, Guid.Empty); + ServerMessengerResolver.Current.Messenger.PerformRefreshAll( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid)); } /// @@ -107,189 +127,27 @@ namespace Umbraco.Web.Cache /// The id. public void Remove(Guid factoryGuid, int id) { - InvokeDispatchMethod(DispatchType.RemoveById, factoryGuid, id, Guid.Empty); + ServerMessengerResolver.Current.Messenger.PerformRemove( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid), + id); } - + /// - /// Used to invoke the method on an ICacheRefresher instance if we are not currently using distributed calls. + /// Sends a request to all registered load-balanced servers to remove the node specified + /// using the specified ICacheRefresher with the guid factoryGuid. /// - /// - /// - /// - /// - private void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, DispatchType dispatchType, int numericId, Guid guidId) + /// + /// + /// + /// + public void Remove(Guid factoryGuid, Func getNumericId, params T[] instances) { - //if we are not, then just invoke the call on the cache refresher - switch (dispatchType) - { - case DispatchType.RefreshAll: - refresher.RefreshAll(); - break; - case DispatchType.RefreshByNumericId: - refresher.Refresh(numericId); - break; - case DispatchType.RefreshByGuid: - refresher.Refresh(guidId); - break; - case DispatchType.RemoveById: - refresher.Remove(numericId); - break; - } - } - - /// - /// Invokes the relevant dispatch method. - /// - /// Type of the dispatch. - /// The factory GUID. - /// The numeric id. - /// The GUID id. - private void InvokeDispatchMethod(DispatchType dispatchType, Guid factoryGuid, int numericId, Guid guidId) - { - //get the refresher, it must be found or else we cannot continue - var refresher = GetRefresherById(factoryGuid); - if (refresher == null) - { - var ex = new InvalidOperationException( - "Could not find an " + typeof(ICacheRefresher).Name + " with the Id " + guidId); - LogHelper.Error("Could not continue with DistributedCache call", ex); - return; - } - - //Now, check if we are using Distrubuted calls - if (!UmbracoSettings.UseDistributedCalls) - { - //if we are not, then just invoke the call on the cache refresher - InvokeMethodOnRefresherInstance(refresher, dispatchType, numericId, guidId); - return; - } - - //We are using distributed calls, so lets make them... - try - { - using (var cacheRefresher = new CacheRefresherClient()) - { - var asyncResultsList = new List(); - - LogStartDispatch(); - - // Go through each configured node submitting a request asynchronously - foreach (XmlNode n in GetDistributedNodes()) - { - SetWebServiceUrlFromNode(cacheRefresher, n); - - // Add the returned WaitHandle to the list for later checking - switch (dispatchType) - { - case DispatchType.RefreshAll: - asyncResultsList.Add(cacheRefresher.BeginRefreshAll(factoryGuid, _login, _password, null, - null)); - break; - case DispatchType.RefreshByGuid: - asyncResultsList.Add(cacheRefresher.BeginRefreshByGuid(factoryGuid, guidId, _login, - _password, null, null)); - break; - case DispatchType.RefreshByNumericId: - asyncResultsList.Add(cacheRefresher.BeginRefreshById(factoryGuid, numericId, _login, - _password, null, null)); - break; - case DispatchType.RemoveById: - asyncResultsList.Add(cacheRefresher.BeginRemoveById(factoryGuid, numericId, _login, - _password, null, null)); - break; - } - } - - - List waitHandlesList; - IAsyncResult[] asyncResults = GetAsyncResults(asyncResultsList, out waitHandlesList); - - int errorCount = 0; - - // Once for each WaitHandle that we have, wait for a response and log it - // We're previously submitted all these requests effectively in parallel and will now retrieve responses on a FIFO basis - for (int waitCalls = 0; waitCalls < asyncResults.Length; waitCalls++) - { - int handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); - - try - { - // Find out if the call succeeded - switch (dispatchType) - { - case DispatchType.RefreshAll: - cacheRefresher.EndRefreshAll(asyncResults[waitCalls]); - break; - case DispatchType.RefreshByGuid: - cacheRefresher.EndRefreshByGuid(asyncResults[waitCalls]); - break; - case DispatchType.RefreshByNumericId: - cacheRefresher.EndRefreshById(asyncResults[waitCalls]); - break; - case DispatchType.RemoveById: - cacheRefresher.EndRemoveById(asyncResults[waitCalls]); - break; - } - } - catch (WebException ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - catch (Exception ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - } - - LogDispatchBatchResult(errorCount); - } - } - catch (Exception ee) - { - LogDispatchBatchError(ee); - } - } - - private void LogDispatchBatchError(Exception ee) - { - LogHelper.Error("Error refreshing distributed list", ee); - } - - private void LogDispatchBatchResult(int errorCount) - { - LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); - } - - private void LogDispatchNodeError(Exception ex) - { - LogHelper.Error("Error refreshing a node in the distributed list", ex); - } - - private void LogDispatchNodeError(WebException ex) - { - string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; - LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); - } - - /// - /// Sets the web service URL for a CacheRefresher from an XmlNode. - /// - /// The CacheRefresher. - /// The XmlNode. - private void SetWebServiceUrlFromNode(WebClientProtocol cr, XmlNode n) - { - string protocol = GlobalSettings.UseSSL ? "https" : "http"; - if (n.Attributes.GetNamedItem("forceProtocol") != null && !String.IsNullOrEmpty(n.Attributes.GetNamedItem("forceProtocol").Value)) - protocol = n.Attributes.GetNamedItem("forceProtocol").Value; - string domain = XmlHelper.GetNodeValue(n); - if (n.Attributes.GetNamedItem("forcePortnumber") != null && !String.IsNullOrEmpty(n.Attributes.GetNamedItem("forcePortnumber").Value)) - domain += string.Format(":{0}", n.Attributes.GetNamedItem("forcePortnumber").Value); - - cr.Url = string.Format("{0}://{1}{2}/cacheRefresher.asmx", protocol, domain, _webServicesUrl); + ServerMessengerResolver.Current.Messenger.PerformRemove( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid), + getNumericId, + instances); } private static ICacheRefresher GetRefresherById(Guid uniqueIdentifier) @@ -297,42 +155,5 @@ namespace Umbraco.Web.Cache return CacheRefreshersResolver.Current.GetById(uniqueIdentifier); } - private void LogStartDispatch() - { - LogHelper.Info("Submitting calls to distributed servers"); - } - - /// - /// Gets the node list of DistributionServers from config. - /// - /// - private XmlNodeList GetDistributedNodes() - { - return UmbracoSettings.DistributionServers.SelectNodes("./server"); - } - - private IAsyncResult[] GetAsyncResults(List asyncResultsList, - out List waitHandlesList) - { - IAsyncResult[] asyncResults = asyncResultsList.ToArray(); - waitHandlesList = new List(); - foreach (IAsyncResult asyncResult in asyncResults) - { - waitHandlesList.Add(asyncResult.AsyncWaitHandle); - } - return asyncResults; - } - - #region Nested type: DispatchType - - private enum DispatchType - { - RefreshAll, - RefreshByNumericId, - RefreshByGuid, - RemoveById - } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 7b6f1c8dae..6f9fb9fafd 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; namespace Umbraco.Web.Cache { @@ -7,6 +10,16 @@ namespace Umbraco.Web.Cache /// public static class DistributedCacheExtensions { + public static void RemoveUserCache(this DistributedCache dc, int userId) + { + dc.Remove(new Guid(DistributedCache.UserCacheRefresherId), userId); + } + + public static void RefreshUserCache(this DistributedCache dc, int userId) + { + dc.Refresh(new Guid(DistributedCache.UserCacheRefresherId), userId); + } + /// /// Refreshes the cache amongst servers for a template /// @@ -40,20 +53,40 @@ namespace Umbraco.Web.Cache /// Refreshes the cache amongst servers for a page /// /// - /// - public static void RefreshPageCache(this DistributedCache dc, int pageId) + /// + public static void RefreshPageCache(this DistributedCache dc, int documentId) { - dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), pageId); + dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), documentId); + } + + /// + /// Refreshes page cache for all instances passed in + /// + /// + /// + public static void RefreshPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); } /// /// Removes the cache amongst servers for a page /// /// - /// - public static void RemovePageCache(this DistributedCache dc, int pageId) + /// + public static void RemovePageCache(this DistributedCache dc, params IContent[] content) { - dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), pageId); + dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); + } + + /// + /// Removes the cache amongst servers for a page + /// + /// + /// + public static void RemovePageCache(this DistributedCache dc, int documentId) + { + dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId); } /// @@ -86,6 +119,16 @@ namespace Umbraco.Web.Cache dc.Refresh(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); } + /// + /// Refreshes the cache amongst servers for a media item + /// + /// + /// + public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) + { + dc.Refresh(new Guid(DistributedCache.MediaCacheRefresherId), x => x.Id, media); + } + /// /// Removes the cache amongst servers for a media item /// @@ -96,6 +139,16 @@ namespace Umbraco.Web.Cache dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); } + /// + /// Removes the cache amongst servers for media items + /// + /// + /// + public static void RemoveMediaCache(this DistributedCache dc, params IMedia[] media) + { + dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), x => x.Id, media); + } + /// /// Refreshes the cache amongst servers for a macro item /// diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index f7f6e0703a..4b578a1fe2 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -1,5 +1,8 @@ using System; +using Umbraco.Core.Models; +using Umbraco.Core.Sync; using umbraco; +using umbraco.cms.businesslogic.web; using umbraco.interfaces; using umbraco.presentation.cache; @@ -12,7 +15,7 @@ namespace Umbraco.Web.Cache /// If Load balancing is enabled (by default disabled, is set in umbracoSettings.config) PageCacheRefresher will be called /// everytime content is added/updated/removed to ensure that the content cache is identical on all load balanced servers /// - public class PageCacheRefresher : ICacheRefresher + public class PageCacheRefresher : ICacheRefresher { /// /// Gets the unique identifier of the CacheRefresher. @@ -69,5 +72,15 @@ namespace Umbraco.Web.Cache { content.Instance.ClearDocumentCache(id); } + + public void Refresh(IContent instance) + { + content.Instance.UpdateDocumentCache(new Document(instance)); + } + + public void Remove(IContent instance) + { + content.Instance.ClearDocumentCache(new Document(instance)); + } } } diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs new file mode 100644 index 0000000000..112024195e --- /dev/null +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -0,0 +1,41 @@ +using System; +using Umbraco.Core; +using umbraco.interfaces; + +namespace Umbraco.Web.Cache +{ + /// + /// Handles User cache invalidation/refreshing + /// + public class UserCacheRefresher : ICacheRefresher + { + public Guid UniqueIdentifier + { + get { return Guid.Parse(DistributedCache.UserCacheRefresherId); } + } + public string Name + { + get { return "User cache refresher"; } + } + + public void RefreshAll() + { + ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch("UmbracoUser"); + } + + public void Refresh(int id) + { + Remove(id); + } + + public void Remove(int id) + { + ApplicationContext.Current.ApplicationCache.ClearCacheItem(string.Format("UmbracoUser{0}", id.ToString())); + } + + public void Refresh(Guid id) + { + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs index 37b72a1e7b..197a88fc56 100644 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs +++ b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs @@ -66,10 +66,7 @@ namespace Umbraco.Web.Strategies.Publishing /// private void UpdateMultipleContentCache(IEnumerable content) { - foreach (var c in content) - { - DistributedCache.Instance.RefreshPageCache(c.Id); - } + DistributedCache.Instance.RefreshPageCache(content.ToArray()); } /// @@ -77,7 +74,7 @@ namespace Umbraco.Web.Strategies.Publishing /// private void UpdateSingleContentCache(IContent content) { - DistributedCache.Instance.RefreshPageCache(content.Id); + DistributedCache.Instance.RefreshPageCache(content); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs index 942f8d7c34..7af7724c7e 100644 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs +++ b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs @@ -51,7 +51,7 @@ namespace Umbraco.Web.Strategies.Publishing /// private void UnPublishSingle(IContent content) { - DistributedCache.Instance.RemovePageCache(content.Id); + DistributedCache.Instance.RemovePageCache(content); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2a468b0059..8004f1e490 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -244,7 +244,6 @@ - @@ -253,6 +252,7 @@ + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 907a8efdb8..25c6d6fc16 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -7,8 +7,10 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Dynamics; +using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Sync; using Umbraco.Web.Dictionary; using Umbraco.Web.Media; using Umbraco.Web.Media.ThumbnailProviders; @@ -16,6 +18,7 @@ using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Routing; +using umbraco.BusinessLogic; using umbraco.businesslogic; using umbraco.cms.businesslogic; using umbraco.presentation.cache; @@ -173,6 +176,24 @@ namespace Umbraco.Web { base.InitializeResolvers(); + //we should not proceed to change this if the app/database is not configured since there will + // be no user, plus we don't need to have server messages sent if this is the case. + if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) + { + var user = User.GetUser(UmbracoSettings.DistributedCallUser); + try + { + //Override the ServerMessengerResolver to set a username/password for the distributed calls + ServerMessengerResolver.Current.SetServerMessenger(new DefaultServerMessenger( + user.LoginName, + user.GetPassword())); + } + catch (Exception e) + { + LogHelper.Error("An error occurred trying to set the IServerMessenger during application startup", e); + } + } + //We are going to manually remove a few cache refreshers here because we've obsoleted them and we don't want them // to be registered more than once CacheRefreshersResolver.Current.RemoveType(); diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index 725b734c13..88fe6965ee 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.WebServices var contentService = (ContentService) Services.ContentService; if (!publishDescendants) { - var result = contentService.SaveAndPublish(doc, false); + var result = contentService.SaveAndPublishInternal(doc); return Json(new { success = result.Success, @@ -40,7 +40,7 @@ namespace Umbraco.Web.WebServices else { var result = ((ContentService) Services.ContentService) - .PublishWithChildren(doc, false, UmbracoUser.Id, includeUnpublished, true) + .PublishWithChildrenInternal(doc, UmbracoUser.Id, includeUnpublished) .ToArray(); return Json(new { diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 42d7b390b4..1d5eb594b7 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -489,7 +489,6 @@ namespace umbraco UpdateDocumentCache(d); } - /// /// Updates the document cache. /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/LiveEditing/Modules/ItemEditing/ItemUpdate.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/LiveEditing/Modules/ItemEditing/ItemUpdate.cs index 1ad595176c..9e1ea7d544 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/LiveEditing/Modules/ItemEditing/ItemUpdate.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/LiveEditing/Modules/ItemEditing/ItemUpdate.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Web.Security; using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.propertytype; using umbraco.cms.businesslogic.web; @@ -96,7 +97,7 @@ namespace umbraco.presentation.LiveEditing.Modules.ItemEditing if(!publishedDocuments.Contains(NodeId.Value)) { Document document = new Document(NodeId.Value); - document.Publish(UmbracoEnsuredPage.CurrentUser); + document.SaveAndPublish(WebSecurity.CurrentUser); publishedDocuments.Add(NodeId.Value); } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs index d2f8d32404..64a78b903e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs @@ -113,7 +113,7 @@ function openContent(id) { node.OpenIcon = dd.ContentTypeIcon; } - if (!dd.Published) + if (!dd.HasPublishedVersion()) node.Style.DimNode(); if (dd.HasPendingChanges()) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/actions/publish.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/actions/publish.aspx.cs index 6c2485db21..303ccfbb03 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/actions/publish.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/actions/publish.aspx.cs @@ -38,7 +38,7 @@ namespace umbraco.presentation.actions deleteMessage.Text = ui.Text("editContentPublishedHeader"); confirm.Visible = false; - d.Publish(getUser()); + d.SaveAndPublish(UmbracoUser); deleted.Text = ui.Text("editContentPublishedHeader") + " ('" + d.Text + "') " + ui.Text("editContentPublishedText") + "

" + ui.Text("view") + " " + d.Text + ""; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs index 987c5965f6..d870b59bec 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs @@ -83,7 +83,7 @@ namespace umbraco.presentation.channels if (publish) { - doc.Publish(new User(username)); + doc.SaveAndPublish(new User(username)); } return true; } @@ -401,7 +401,7 @@ namespace umbraco.presentation.channels if (publish) { - doc.Publish(new User(username)); + doc.SaveAndPublish(new User(username)); } return doc.Id.ToString(); } 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 d2ecd05a33..23f43d7685 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -1,9 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Web; +using System.Web.Script.Serialization; using System.Web.Services; using System.Xml; using Umbraco.Core; @@ -82,6 +84,29 @@ namespace umbraco.presentation.webservices } } + ///

+ /// Refreshes objects for all Ids matched in the json string + /// + /// + /// A JSON Serialized string of ids to match + /// + /// + [WebMethod] + public void RefreshByIds(Guid uniqueIdentifier, string jsonIds, string Login, string Password) + { + var serializer = new JavaScriptSerializer(); + var ids = serializer.Deserialize(jsonIds); + + if (BusinessLogic.User.validateCredentials(Login, Password)) + { + var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); + foreach (var i in ids) + { + cr.Refresh(i); + } + } + } + [WebMethod] public void RemoveById(Guid uniqueIdentifier, int Id, string Login, string Password) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs index eda8ee7bee..e767cf5fd9 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs @@ -7,6 +7,7 @@ using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Caching; using Umbraco.Web; +using Umbraco.Web.Security; using umbraco.BasePages; using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.web; @@ -98,7 +99,7 @@ namespace umbraco.presentation.webservices // refresh the xml for the sorting to work if (published) { - document.Publish(BusinessLogic.User.GetCurrent()); + document.SaveAndPublish(WebSecurity.CurrentUser); document.refreshXmlSortOrder(); } } diff --git a/src/UmbracoExamine/BaseUmbracoIndexer.cs b/src/UmbracoExamine/BaseUmbracoIndexer.cs index dc6c1c704f..fd9a1e8c16 100644 --- a/src/UmbracoExamine/BaseUmbracoIndexer.cs +++ b/src/UmbracoExamine/BaseUmbracoIndexer.cs @@ -95,7 +95,7 @@ namespace UmbracoExamine { //We need to check if we actually can initialize, if not then don't continue - if (!CanInitialized()) + if (!CanInitialize()) { return; } @@ -173,7 +173,8 @@ namespace UmbracoExamine /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes ///
/// - protected bool CanInitialized() + [SecuritySafeCritical] + protected bool CanInitialize() { //We need to check if we actually can initialize, if not then don't continue if (ApplicationContext.Current == null diff --git a/src/UmbracoExamine/DataServices/UmbracoMediaService.cs b/src/UmbracoExamine/DataServices/UmbracoMediaService.cs index a324903c41..c1bd2c71d3 100644 --- a/src/UmbracoExamine/DataServices/UmbracoMediaService.cs +++ b/src/UmbracoExamine/DataServices/UmbracoMediaService.cs @@ -19,12 +19,14 @@ namespace UmbracoExamine.DataServices { private readonly ServiceContext _services; + [SecuritySafeCritical] public UmbracoMediaService() : this(ApplicationContext.Current.Services) { } + [SecuritySafeCritical] public UmbracoMediaService(ServiceContext services) { _services = services; diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index 1fb1a19c69..7d3f22a187 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -121,7 +121,7 @@ namespace UmbracoExamine { //We need to check if we actually can initialize, if not then don't continue - if (!CanInitialized()) + if (!CanInitialize()) { return; } diff --git a/src/UmbracoExamine/UmbracoExamineSearcher.cs b/src/UmbracoExamine/UmbracoExamineSearcher.cs index b0fc1a54e9..a123bb4160 100644 --- a/src/UmbracoExamine/UmbracoExamineSearcher.cs +++ b/src/UmbracoExamine/UmbracoExamineSearcher.cs @@ -34,7 +34,7 @@ namespace UmbracoExamine public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { //We need to check if we actually can initialize, if not then don't continue - if (!CanInitialized()) + if (!CanInitialize()) { return; } @@ -70,7 +70,8 @@ namespace UmbracoExamine /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes ///
/// - protected bool CanInitialized() + [SecuritySafeCritical] + protected bool CanInitialize() { //We need to check if we actually can initialize, if not then don't continue if (ApplicationContext.Current == null diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 3320e4fd9c..0f1e5d4946 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using Umbraco.Core; using Umbraco.Core.Logging; using umbraco.DataLayer; using System.Collections.Generic; @@ -645,7 +646,7 @@ namespace umbraco.BusinessLogic { this.LoginName = DateTime.Now.ToString("yyyyMMdd") + "_" + this.LoginName; } - + this.Save(); } /// @@ -890,12 +891,11 @@ namespace umbraco.BusinessLogic /// /// Flushes the user from cache. /// + [Obsolete("This method should not be used, cache flushing is handled automatically by event handling in the web application and ensures that all servers are notified, this will not notify all servers in a load balanced environment")] public void FlushFromCache() { OnFlushingFromCache(EventArgs.Empty); - - if (System.Web.HttpRuntime.Cache[string.Format("UmbracoUser{0}", Id.ToString())] != null) - System.Web.HttpRuntime.Cache.Remove(string.Format("UmbracoUser{0}", Id.ToString())); + ApplicationContext.Current.ApplicationCache.ClearCacheItem(string.Format("UmbracoUser{0}", Id.ToString())); } /// @@ -905,22 +905,19 @@ namespace umbraco.BusinessLogic /// public static User GetUser(int id) { - if (System.Web.HttpRuntime.Cache[string.Format("UmbracoUser{0}", id.ToString())] == null) - { - - try - { - User u = new User(id); - System.Web.HttpRuntime.Cache.Insert(string.Format("UmbracoUser{0}", id.ToString()), u); - } - catch (ArgumentException) - { - //no user was found - return null; - } - - } - return (User)System.Web.HttpRuntime.Cache[string.Format("UmbracoUser{0}", id.ToString())]; + return ApplicationContext.Current.ApplicationCache.GetCacheItem( + string.Format("UmbracoUser{0}", id.ToString()), () => + { + try + { + return new User(id); + } + catch (ArgumentException) + { + //no user was found + return null; + } + }); } diff --git a/src/umbraco.cms/businesslogic/skinning/tasks/ModifyPageProperty.cs b/src/umbraco.cms/businesslogic/skinning/tasks/ModifyPageProperty.cs index 933715c4ad..cba9da77fb 100644 --- a/src/umbraco.cms/businesslogic/skinning/tasks/ModifyPageProperty.cs +++ b/src/umbraco.cms/businesslogic/skinning/tasks/ModifyPageProperty.cs @@ -34,7 +34,7 @@ namespace umbraco.cms.businesslogic.skinning.tasks d.OriginalValue = doc.getProperty(PropertyAlias).Value.ToString(); doc.getProperty(PropertyAlias).Value = Value; - doc.Publish(new BusinessLogic.User(0)); + doc.SaveAndPublish(new BusinessLogic.User(0)); d.NewValue = Value; d.TaskExecutionStatus = TaskExecutionStatus.Completed; diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index 4d11f82e47..93b1dd95e3 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -801,7 +801,7 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - var result = ((ContentService)ApplicationContext.Current.Services.ContentService).Publish(Content, false, u.Id); + var result = ((ContentService)ApplicationContext.Current.Services.ContentService).PublishInternal(Content, u.Id); _published = result.Success; FireAfterPublish(e); @@ -817,7 +817,8 @@ namespace umbraco.cms.businesslogic.web [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentService.PublishWithChildren()", false)] public bool PublishWithChildrenWithResult(User u) { - var result = ((ContentService)ApplicationContext.Current.Services.ContentService).PublishWithChildren(Content, false, u.Id); + var result = ((ContentService)ApplicationContext.Current.Services.ContentService) + .PublishWithChildrenInternal(Content, u.Id); //This used to just return false only when the parent content failed, otherwise would always return true so we'll // do the same thing for the moment return result.Single(x => x.Result.ContentItem.Id == Id).Success; @@ -857,7 +858,8 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - var publishedResults = ((ContentService)ApplicationContext.Current.Services.ContentService).PublishWithChildren(Content, false, u.Id); + var publishedResults = ((ContentService)ApplicationContext.Current.Services.ContentService) + .PublishWithChildrenInternal(Content, u.Id); FireAfterPublish(e); } @@ -872,7 +874,7 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - _published = ((ContentService)ApplicationContext.Current.Services.ContentService).UnPublish(Content, false, 0); + _published = ((ContentService)ApplicationContext.Current.Services.ContentService).UnPublish(Content); FireAfterUnPublish(e); } @@ -931,7 +933,7 @@ namespace umbraco.cms.businesslogic.web { //NOTE: The 'false' parameter will cause the PublishingStrategy events to fire which will ensure that the cache is refreshed. var result = ((ContentService)ApplicationContext.Current.Services.ContentService) - .SaveAndPublish(Content, false, u.Id); + .SaveAndPublishInternal(Content, u.Id); //NOTE: This is just going to call the CMSNode Save which will launch into the CMSNode.BeforeSave and CMSNode.AfterSave evenths // which actually do dick all and there's no point in even having them there but just in case for some insane reason someone diff --git a/src/umbraco.interfaces/ICacheRefresher.cs b/src/umbraco.interfaces/ICacheRefresher.cs index a402682b33..21f454ca66 100644 --- a/src/umbraco.interfaces/ICacheRefresher.cs +++ b/src/umbraco.interfaces/ICacheRefresher.cs @@ -7,9 +7,10 @@ namespace umbraco.interfaces /// The IcacheRefresher Interface is used for loadbalancing. /// /// - public interface ICacheRefresher { - Guid UniqueIdentifier { get;} - string Name { get;} + public interface ICacheRefresher + { + Guid UniqueIdentifier { get; } + string Name { get; } void RefreshAll(); void Refresh(int Id); void Remove(int Id); diff --git a/src/umbraco.providers/UsersMembershipProvider.cs b/src/umbraco.providers/UsersMembershipProvider.cs index e0216d0ccb..e90d7b2495 100644 --- a/src/umbraco.providers/UsersMembershipProvider.cs +++ b/src/umbraco.providers/UsersMembershipProvider.cs @@ -248,6 +248,7 @@ namespace umbraco.providers User user = new User(username); string encodedPassword = EncodePassword(newPassword); user.Password = encodedPassword; + user.Save(); return (user.ValidatePassword(encodedPassword)) ? true : false; } @@ -525,6 +526,7 @@ namespace umbraco.providers { User user = new User(userName); user.Disabled = false; + user.Save(); } catch (Exception) {