diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index cb89ace510..a51da5652e 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Events /// /// This is protected so that inheritors can expose it with their own name /// - protected T EventObject { get; private set; } + protected T EventObject { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 59e7e19f46..107629ff19 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -1,28 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; + namespace Umbraco.Core.Events { - public class MoveEventArgs : CancellableObjectEventArgs - { - public MoveEventArgs(TEntity eventObject, bool canCancel, int parentId) : base(eventObject, canCancel) - { - ParentId = parentId; - } + public class MoveEventInfo + { + public MoveEventInfo(TEntity entity, string originalPath, int newParentId) + { + Entity = entity; + OriginalPath = originalPath; + NewParentId = newParentId; + } - public MoveEventArgs(TEntity eventObject, int parentId) : base(eventObject) - { - ParentId = parentId; - } + public TEntity Entity { get; set; } + public string OriginalPath { get; set; } + public int NewParentId { get; set; } + } - /// - /// The entity being moved - /// - public TEntity Entity - { - get { return EventObject; } - } + public class MoveEventArgs : CancellableObjectEventArgs + { + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A colleciton of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, params MoveEventInfo[] moveInfo) + : base(default(TEntity), canCancel) + { + if (moveInfo.FirstOrDefault() == null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } - /// - /// Gets or Sets the Id of the objects new parent. - /// - public int ParentId { get; private set; } - } + MoveInfoCollection = moveInfo; + //assign the legacy props + EventObject = moveInfo.First().Entity; + ParentId = moveInfo.First().NewParentId; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// A colleciton of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(params MoveEventInfo[] moveInfo) + : base(default(TEntity)) + { + if (moveInfo.FirstOrDefault() == null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + //assign the legacy props + EventObject = moveInfo.First().Entity; + ParentId = moveInfo.First().NewParentId; + } + + [Obsolete("Use the overload that specifies the MoveEventInfo object")] + public MoveEventArgs(TEntity eventObject, bool canCancel, int parentId) + : base(eventObject, canCancel) + { + ParentId = parentId; + } + + [Obsolete("Use the overload that specifies the MoveEventInfo object")] + public MoveEventArgs(TEntity eventObject, int parentId) + : base(eventObject) + { + ParentId = parentId; + } + + /// + /// Gets all MoveEventInfo objects used to create the object + /// + public IEnumerable> MoveInfoCollection { get; private set; } + + /// + /// The entity being moved + /// + [Obsolete("Retrieve the entity object from the MoveInfoCollection property instead")] + public TEntity Entity + { + get { return EventObject; } + } + + /// + /// Gets the Id of the object's new parent + /// + [Obsolete("Retrieve the ParentId from the MoveInfoCollection property instead")] + public int ParentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e9d347c970..f6f38d6002 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -968,8 +968,19 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(content, -20), this)) + var originalPath = content.Path; + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) + { return; + } + + var moveInfo = new List> + { + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) + }; //Make sure that published content is unpublished before being moved to the Recycle Bin if (HasPublishedVersion(content.Id)) @@ -994,6 +1005,8 @@ namespace Umbraco.Core.Services //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); @@ -1002,7 +1015,7 @@ namespace Umbraco.Core.Services uow.Commit(); } - Trashed.RaiseEvent(new MoveEventArgs(content, false, -20), this); + Trashed.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); Audit.Add(AuditTypes.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); } @@ -1029,76 +1042,21 @@ namespace Umbraco.Core.Services MoveToRecycleBin(content, userId); return; } - - if (Moving.IsRaisedEventCancelled(new MoveEventArgs(content, parentId), this)) + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(content, content.Path, parentId)), this)) + { return; - - content.WriterId = userId; - if (parentId == -1) - { - content.Path = string.Concat("-1,", content.Id); - content.Level = 1; - } - else - { - var parent = GetById(parentId); - content.Path = string.Concat(parent.Path, ",", content.Id); - content.Level = parent.Level + 1; } + //used to track all the moved entities to be given to the event + var moveInfo = new List>(); - //If Content is being moved away from Recycle Bin, its state should be un-trashed - if (content.Trashed && parentId != -20) - { - content.ChangeTrashedState(false, parentId); - } - else - { - content.ParentId = parentId; - } + //call private method that does the recursive moving + PerformMove(content, parentId, userId, moveInfo); - //If Content is published, it should be (re)published from its new location - if (content.Published) - { - //If Content is Publishable its saved and published - //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. - if (IsPublishable(content)) - { - SaveAndPublish(content, userId); - } - else - { - Save(content, false, userId, true); - - using (var uow = _uowProvider.GetUnitOfWork()) - { - var xml = content.ToXml(); - var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; - var exists = - uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != - null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); - } - } - } - else - { - Save(content, userId); - } - - //Ensure that Path and Level is updated on children - var children = GetChildren(content.Id).ToArray(); - if (children.Any()) - { - foreach (var child in children) - { - Move(child, content.Id, userId); - } - } - - Moved.RaiseEvent(new MoveEventArgs(content, false, parentId), this); + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); Audit.Add(AuditTypes.Move, "Move Content performed by user", userId, content.Id); } @@ -1145,6 +1103,9 @@ namespace Umbraco.Core.Services /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) { + //TODO: This all needs to be managed correctly so that the logic is submitted in one + // transaction, the CRUD needs to be moved to the repo + using (new WriteLock(Locker)) { var copy = content.DeepCloneWithResetIdentities(); @@ -1167,6 +1128,7 @@ namespace Umbraco.Core.Services uow.Commit(); //Special case for the Upload DataType + //TODO: This really shouldn't be here! What about the cropper in v7, we'll have the same issue. var uploadDataTypeId = new Guid(Constants.PropertyEditors.UploadField); if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId)) { @@ -1461,6 +1423,79 @@ namespace Umbraco.Core.Services #region Private Methods + private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) + { + //add a tracking item to use in the Moved event + moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); + + content.WriterId = userId; + if (parentId == -1) + { + content.Path = string.Concat("-1,", content.Id); + content.Level = 1; + } + else + { + var parent = GetById(parentId); + content.Path = string.Concat(parent.Path, ",", content.Id); + content.Level = parent.Level + 1; + } + + //If Content is being moved away from Recycle Bin, its state should be un-trashed + if (content.Trashed && parentId != Constants.System.RecycleBinContent) + { + content.ChangeTrashedState(false, parentId); + } + else + { + content.ParentId = parentId; + } + + //If Content is published, it should be (re)published from its new location + if (content.Published) + { + //If Content is Publishable its saved and published + //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. + if (IsPublishable(content)) + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + SaveAndPublish(content, userId); + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, false, userId); + + using (var uow = _uowProvider.GetUnitOfWork()) + { + var xml = content.ToXml(); + var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; + var exists = + uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != + null; + int result = exists + ? uow.Database.Update(poco) + : Convert.ToInt32(uow.Database.Insert(poco)); + } + } + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, userId); + } + + //Ensure that Path and Level is updated on children + var children = GetChildren(content.Id).ToArray(); + if (children.Any()) + { + foreach (var child in children) + { + PerformMove(child, content.Id, userId, moveInfo); + } + } + } + //TODO: WE should make a base class for ContentService and MediaService to share! // currently we have this logic duplicated (nearly the same) for media types and soon to be member types diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index dfeb271936..865a8416cc 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -516,15 +516,29 @@ namespace Umbraco.Core.Services return; } - if (Moving.IsRaisedEventCancelled(new MoveEventArgs(media, parentId), this)) - return; + var originalPath = media.Path; + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(media, originalPath, parentId)), this)) + { + return; + } + media.ParentId = parentId; if (media.Trashed) { media.ChangeTrashedState(false, parentId); } - Save(media, userId); + Save(media, userId, + //no events! + false); + + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, parentId) + }; //Ensure that relevant properties are updated on children var children = GetChildren(media.Id).ToArray(); @@ -533,11 +547,13 @@ namespace Umbraco.Core.Services var parentPath = media.Path; var parentLevel = media.Level; var parentTrashed = media.Trashed; - var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed); - Save(updatedDescendants, userId); + var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); + Save(updatedDescendants, userId, + //no events! + false); } - Moved.RaiseEvent(new MoveEventArgs(media, false, parentId), this); + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); Audit.Add(AuditTypes.Move, "Move Media performed by user", userId, media.Id); } @@ -552,8 +568,19 @@ namespace Umbraco.Core.Services { if (media == null) throw new ArgumentNullException("media"); - if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(media, -21), this)) + var originalPath = media.Path; + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) + { return; + } + + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) + }; //Find Descendants, which will be moved to the recycle bin along with the parent/grandparent. var descendants = GetDescendants(media).OrderBy(x => x.Level).ToList(); @@ -561,12 +588,14 @@ namespace Umbraco.Core.Services var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMediaRepository(uow)) { + //TODO: This should be part of the repo! + //Remove 'published' xml from the cmsContentXml table for the unpublished media uow.Database.Delete("WHERE nodeId = @Id", new { Id = media.Id }); - media.ChangeTrashedState(true, -21); + media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); repository.AddOrUpdate(media); - + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { @@ -575,12 +604,14 @@ namespace Umbraco.Core.Services descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); + + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } uow.Commit(); } - Trashed.RaiseEvent(new MoveEventArgs(media, false, -21), this); + Trashed.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); Audit.Add(AuditTypes.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); } @@ -978,12 +1009,14 @@ namespace Umbraco.Core.Services /// Path of the Parent media /// Level of the Parent media /// Indicates whether the Parent is trashed or not + /// Used to track the objects to be used in the move event /// Collection of updated objects - private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed) + private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) { var list = new List(); foreach (var child in children) { + var originalPath = child.Path; child.Path = string.Concat(parentPath, ",", child.Id); child.Level = parentLevel + 1; if (parentTrashed != child.Trashed) @@ -991,12 +1024,13 @@ namespace Umbraco.Core.Services child.ChangeTrashedState(parentTrashed, child.ParentId); } + eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); list.Add(child); var grandkids = GetChildren(child.Id).ToArray(); if (grandkids.Any()) { - list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed)); + list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); } } return list; diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 469463d7a2..3d8da381d1 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Events; @@ -123,23 +124,22 @@ namespace Umbraco.Web.Cache //Bind to media events - MediaService.Saved += MediaServiceSaved; - //We need to perform all of the 'before' events here because we need a reference to the - //media item's Path before it is moved/deleting/trashed - //see: http://issues.umbraco.org/issue/U4-1653 - MediaService.Deleting += MediaServiceDeleting; - MediaService.Moving += MediaServiceMoving; - MediaService.Trashing += MediaServiceTrashing; + MediaService.Saved += MediaServiceSaved; + MediaService.Deleted += MediaServiceDeleted; + MediaService.Moved += MediaServiceMoved; + MediaService.Trashed += MediaServiceTrashed; + MediaService.EmptiedRecycleBin += MediaServiceEmptiedRecycleBin; //Bind to content events - this is for unpublished content syncing across servers (primarily for examine) + ContentService.EmptiedRecycleBin += ContentServiceEmptiedRecycleBin; 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. - + //TODO: The Move method of the content service fires Saved/Published events during its execution so we don't need to listen to moved + //ContentService.Moved += ContentServiceMoved; + ContentService.Trashed += ContentServiceTrashed; + //public access events Access.AfterSave += Access_AfterSave; } @@ -155,8 +155,31 @@ namespace Umbraco.Web.Cache #region Content service event handlers + static void ContentServiceEmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) + { + if (e.RecycleBinEmptiedSuccessfully && e.IsContentRecycleBin) + { + DistributedCache.Instance.RemoveUnpublishedCachePermanently(e.Ids.ToArray()); + } + } + /// - /// Handles cache refreshgi for when content is copied + /// Handles cache refreshing for when content is trashed + /// + /// + /// + /// + /// This is for the unpublished page refresher - the entity will be unpublished before being moved to the trash + /// and the unpublished event will take care of remove it from any published caches + /// + static void ContentServiceTrashed(IContentService sender, MoveEventArgs e) + { + DistributedCache.Instance.RefreshUnpublishedPageCache( + e.MoveInfoCollection.Select(x => x.Entity).ToArray()); + } + + /// + /// Handles cache refreshing for when content is copied /// /// /// @@ -173,7 +196,7 @@ namespace Umbraco.Web.Cache DistributedCache.Instance.RefreshAllUserPermissionsCache(); } - //run the un-published cache refresher + //run the un-published cache refresher since copied content is not published DistributedCache.Instance.RefreshUnpublishedPageCache(e.Copy); } @@ -606,19 +629,28 @@ namespace Umbraco.Web.Cache #endregion #region Media event handlers - static void MediaServiceTrashing(IMediaService sender, MoveEventArgs e) + + static void MediaServiceEmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(false, e.Entity); + if (e.RecycleBinEmptiedSuccessfully && e.IsMediaRecycleBin) + { + DistributedCache.Instance.RemoveMediaCachePermanently(e.Ids.ToArray()); + } } - static void MediaServiceMoving(IMediaService sender, MoveEventArgs e) + static void MediaServiceTrashed(IMediaService sender, MoveEventArgs e) { - DistributedCache.Instance.RefreshMediaCache(e.Entity); + DistributedCache.Instance.RemoveMediaCacheAfterRecycling(e.MoveInfoCollection.ToArray()); } - static void MediaServiceDeleting(IMediaService sender, DeleteEventArgs e) + static void MediaServiceMoved(IMediaService sender, MoveEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(true, e.DeletedEntities.ToArray()); + DistributedCache.Instance.RefreshMediaCacheAfterMoving(e.MoveInfoCollection.ToArray()); + } + + static void MediaServiceDeleted(IMediaService sender, DeleteEventArgs e) + { + DistributedCache.Instance.RemoveMediaCachePermanently(e.DeletedEntities.Select(x => x.Id).ToArray()); } static void MediaServiceSaved(IMediaService sender, SaveEventArgs e) diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 7263776ae5..73234ed1a8 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Events; using Umbraco.Core.Models; using umbraco; using umbraco.cms.businesslogic.web; @@ -264,6 +265,17 @@ namespace Umbraco.Web.Cache dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); } + /// + /// invokes the unpublished page cache refresher to mark all ids for permanent removal + /// + /// + /// + public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds) + { + dc.RefreshByJson(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), + UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); + } + #endregion #region Member cache @@ -338,7 +350,7 @@ namespace Umbraco.Web.Cache #region Media Cache /// - /// Refreshes the cache amongst servers for a media item + /// Refreshes the cache amongst servers for media items /// /// /// @@ -348,6 +360,18 @@ namespace Umbraco.Web.Cache MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); } + /// + /// Refreshes the cache amongst servers for a media item after it's been moved + /// + /// + /// + public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo[] media) + { + dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), + MediaCacheRefresher.SerializeToJsonPayloadForMoving( + MediaCacheRefresher.OperationType.Saved, media)); + } + /// /// Removes the cache amongst servers for a media item /// @@ -365,18 +389,27 @@ namespace Umbraco.Web.Cache } /// - /// Removes the cache amongst servers for media items + /// Removes the cache among servers for media items when they are recycled /// /// - /// /// - public static void RemoveMediaCache(this DistributedCache dc, bool isPermanentlyDeleted, params IMedia[] media) + public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo[] media) { dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload( - isPermanentlyDeleted ? MediaCacheRefresher.OperationType.Deleted : MediaCacheRefresher.OperationType.Trashed, - media)); - } + MediaCacheRefresher.SerializeToJsonPayloadForMoving( + MediaCacheRefresher.OperationType.Trashed, media)); + } + + /// + /// Removes the cache among servers for media items when they are permanently deleted + /// + /// + /// + public static void RemoveMediaCachePermanently(this DistributedCache dc, params int[] mediaIds) + { + dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), + MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); + } #endregion diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index a2de17626f..9074e4856c 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Web.Script.Serialization; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Models; using umbraco.interfaces; @@ -47,6 +48,31 @@ namespace Umbraco.Web.Cache return json; } + internal static string SerializeToJsonPayloadForMoving(OperationType operation, MoveEventInfo[] media) + { + var serializer = new JavaScriptSerializer(); + var items = media.Select(x => new JsonPayload + { + Id = x.Entity.Id, + Operation = operation, + Path = x.OriginalPath + }).ToArray(); + var json = serializer.Serialize(items); + return json; + } + + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] mediaIds) + { + var serializer = new JavaScriptSerializer(); + var items = mediaIds.Select(x => new JsonPayload + { + Id = x, + Operation = OperationType.Deleted + }).ToArray(); + var json = serializer.Serialize(items); + return json; + } + /// /// Converts a macro to a jsonPayload object /// @@ -129,17 +155,26 @@ namespace Umbraco.Web.Cache payloads.ForEach(payload => { - foreach (var idPart in payload.Path.Split(',')) + //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) + if (payload.Path.IsNullOrWhiteSpace()) { ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( - string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); - - // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); } + else + { + foreach (var idPart in payload.Path.Split(',')) + { + ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( + string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); + + // Also clear calls that only query this specific item! + 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 index 4413a61e9b..765c591415 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -1,14 +1,24 @@ using System; +using System.Web.Script.Serialization; using Umbraco.Core.Cache; using Umbraco.Core.Models; +using System.Linq; +using Umbraco.Core.Sync; 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 + public sealed class UnpublishedPageCacheRefresher : TypedCacheRefresherBase, IJsonCacheRefresher { + + //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 + protected override UnpublishedPageCacheRefresher Instance { get { return this; } @@ -24,10 +34,58 @@ namespace Umbraco.Web.Cache 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 + #region Static helpers + + /// + /// Converts the json to a JsonPayload object + /// + /// + /// + internal static JsonPayload[] DeserializeFromJsonPayload(string json) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.Deserialize(json); + return jsonObject; + } + + + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds) + { + var serializer = new JavaScriptSerializer(); + var items = contentIds.Select(x => new JsonPayload + { + Id = x, + Operation = OperationType.Deleted + }).ToArray(); + var json = serializer.Serialize(items); + return json; + } + + #endregion + + #region Sub classes + + internal enum OperationType + { + Deleted + } + + internal class JsonPayload + { + public int Id { get; set; } + public OperationType Operation { get; set; } + } + + #endregion + + /// + /// Implement the IJsonCacheRefresher so that we can bulk delete the cache based on multiple IDs for when the recycle bin is emptied + /// + /// + public void Refresh(string jsonPayload) + { + OnCacheUpdated(Instance, new CacheRefresherEventArgs(jsonPayload, MessageType.RefreshByJson)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index c04920e289..4507a1a92a 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -158,19 +158,33 @@ namespace Umbraco.Web.Search switch (payload.Operation) { case MediaCacheRefresher.OperationType.Saved: - var media = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); - if (media != null) + var media1 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media1 != null) { - ReIndexForMedia(media, media.Trashed == false); + ReIndexForMedia(media1, media1.Trashed == false); } break; case MediaCacheRefresher.OperationType.Trashed: + //keep if trashed for indexes supporting unpublished + //(delete the index from all indexes not supporting unpublished content) + DeleteIndexForEntity(payload.Id, true); + + //We then need to re-index this item for all indexes supporting unpublished content + var media2 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media2 != null) + { + ReIndexForMedia(media2, false); + } + break; case MediaCacheRefresher.OperationType.Deleted: + //permanently remove from all indexes + DeleteIndexForEntity(payload.Id, false); + break; default: throw new ArgumentOutOfRangeException();