diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index 6404d60b40..bf58454bfc 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Models; using System.Linq; using Newtonsoft.Json; +using umbraco.cms.businesslogic.web; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Sync; @@ -44,7 +45,7 @@ namespace Umbraco.Web.Cache return jsonObject; } - + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds) { var items = contentIds.Select(x => new JsonPayload @@ -61,12 +62,12 @@ namespace Umbraco.Web.Cache #region Sub classes internal enum OperationType - { + { Deleted } internal class JsonPayload - { + { public int Id { get; set; } public OperationType Operation { get; set; } } @@ -79,6 +80,7 @@ namespace Umbraco.Web.Cache ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(); base.RefreshAll(); } @@ -87,6 +89,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); + var d = new Document(id); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(id); } @@ -97,6 +102,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(id); base.Remove(id); } @@ -106,6 +112,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); + var d = new Document(instance); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(instance); } @@ -116,6 +125,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(instance.Id); base.Remove(instance); } @@ -132,6 +142,7 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); ClearRepositoryCacheItemById(payload.Id); content.Instance.UpdateSortOrder(payload.Id); + content.Instance.ClearPreviewXmlContent(payload.Id); } DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 59772e8237..e9342b6136 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -523,6 +523,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (preview) { + if (PreviewContent.IsSinglePreview) + return content.Instance.PreviewXmlContent; var previewContent = PreviewContentCache.GetOrCreateValue(context); // will use the ctor with no parameters previewContent.EnsureInitialized(context.UmbracoUser, StateHelper.Cookies.Preview.GetValue(), true, () => { diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ccd2e009a7..deb3f0be17 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; +using System.Threading; using System.Web; using System.Xml; using umbraco.BusinessLogic; @@ -13,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.web; using umbraco.DataLayer; using umbraco.presentation.nodeFactory; +using umbraco.presentation.preview; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -275,6 +277,8 @@ namespace umbraco safeXml.AcceptChanges(); } + + ClearPreviewXmlContent(); } /// @@ -474,6 +478,8 @@ namespace umbraco safeXml.AcceptChanges(); } } + + ClearPreviewXmlContent(id); } /// @@ -668,6 +674,21 @@ namespace umbraco _persisterTask = _persisterTask.Touch(); // _persisterTask != null because SyncToXmlFile == true } + private static bool HasSchema(string contentTypeAlias, XmlDocument xml) + { + string subset = null; + + // get current doctype + var n = xml.FirstChild; + while (n.NodeType != XmlNodeType.DocumentType && n.NextSibling != null) + n = n.NextSibling; + if (n.NodeType == XmlNodeType.DocumentType) + subset = ((XmlDocumentType)n).InternalSubset; + + // ensure it contains the content type + return subset != null && subset.Contains(string.Format("", contentTypeAlias)); + } + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -1260,5 +1281,191 @@ namespace umbraco } #endregion + + #region Preview + + private const string PreviewCacheKey = "umbraco.content.preview"; + + internal void ClearPreviewXmlContent() + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + runtimeCache.ClearCacheItem(PreviewCacheKey); + } + + internal void ClearPreviewXmlContent(int id) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // Check if node present, before cloning + var x = xml.GetElementById(id.ToString()); + if (x == null) + return; + + // Find the document in the xml cache + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + } + + internal void UpdatePreviewXmlContent(Document d) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + var pnode = GetPreviewOrPublishedNode(d, xml, true); + var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); + pattr.Value = d.sortOrder.ToString(); + AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + } + + private void AddOrUpdatePreviewXmlNode(int id, int level, int parentId, XmlNode docNode) + { + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // sanity checks + if (id != docNode.AttributeValue("id")) + throw new ArgumentException("Values of id and docNode/@id are different."); + if (parentId != docNode.AttributeValue("parentID")) + throw new ArgumentException("Values of parentId and docNode/@parentID are different."); + + // find the document in the cache + XmlNode currentNode = xml.GetElementById(id.ToInvariantString()); + + // if the document is not there already then it's a new document + // we must make sure that its document type exists in the schema + if (currentNode == null && UseLegacySchema == false) + { + if (HasSchema(docNode.Name, xml) == false) + { + runtimeCache.ClearCacheItem(PreviewCacheKey); + return; + } + } + + // find the parent + XmlNode parentNode = level == 1 + ? xml.DocumentElement + : xml.GetElementById(parentId.ToInvariantString()); + + // no parent = cannot do anything + if (parentNode == null) + return; + + // insert/move the node under the parent + if (currentNode == null) + { + // document not there, new node, append + currentNode = docNode; + parentNode.AppendChild(currentNode); + } + else + { + // document found... we could just copy the currentNode children nodes over under + // docNode, then remove currentNode and insert docNode... the code below tries to + // be clever and faster, though only benchmarking could tell whether it's worth the + // pain... + + // first copy current parent ID - so we can compare with target parent + var moving = currentNode.AttributeValue("parentID") != parentId; + + if (docNode.Name == currentNode.Name) + { + // name has not changed, safe to just update the current node + // by transfering values eg copying the attributes, and importing the data elements + TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode); + + // if moving, move the node to the new parent + // else it's already under the right parent + // (but maybe the sort order has been updated) + if (moving) + parentNode.AppendChild(currentNode); // remove then append to parentNode + } + else + { + // name has changed, must use docNode (with new name) + // move children nodes from currentNode to docNode (already has properties) + var children = currentNode.SelectNodes(ChildNodesXPath); + if (children == null) throw new Exception("oops"); + foreach (XmlNode child in children) + docNode.AppendChild(child); // remove then append to docNode + + // and put docNode in the right place - if parent has not changed, then + // just replace, else remove currentNode and insert docNode under the right parent + // (but maybe not at the right position due to sort order) + if (moving) + { + if (currentNode.ParentNode == null) throw new Exception("oops"); + currentNode.ParentNode.RemoveChild(currentNode); + parentNode.AppendChild(docNode); + } + else + { + // replacing might screw the sort order + parentNode.ReplaceChild(docNode, currentNode); + } + + currentNode = docNode; + } + } + + // if the nodes are not ordered, must sort + // (see U4-509 + has to work with ReplaceChild too) + //XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue("sortOrder")); + + // but... + // if we assume that nodes are always correctly sorted + // then we just need to ensure that currentNode is at the right position. + // should be faster that moving all the nodes around. + XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + } + + // UpdateSortOrder is meant to update the Xml cache sort order on Save, 'cos that change + // should be applied immediately, even though the Xml cache is not updated on Saves - we + // don't have to do it for preview Xml since it is always fully updated - OTOH we have + // to ensure it *is* updated, in UnpublishedPageCacheRefresher + + private XmlDocument LoadPreviewXmlContent() + { + try + { + LogHelper.Info("Loading preview content from database..."); + var xml = ApplicationContext.Current.Services.ContentService.BuildPreviewXmlCache(); + LogHelper.Debug("Done loading preview content"); + return xml; + } + catch (Exception ee) + { + LogHelper.Error("Error loading preview content", ee); + } + + // An error of some sort must have stopped us from successfully generating + // the content tree, so lets return null signifying there is no content available + return null; + } + + public XmlDocument PreviewXmlContent + { + get + { + if (PreviewContent.IsSinglePreview == false) + throw new InvalidOperationException(); + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, + removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + } + } + + #endregion } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 3346fa608c..815db997f0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Globalization; using System.IO; using System.Xml; @@ -11,11 +12,57 @@ using Umbraco.Core.Logging; namespace umbraco.presentation.preview { - //TODO : Migrate this to a new API! + public enum PreviewMode + { + Unknown = 0, // default value + Vintage, + SinglePreview + } public class PreviewContent { - // zb-00004 #29956 : refactor cookies names & handling + private static PreviewMode _previewMode; + private const PreviewMode DefaultPreviewMode = PreviewMode.SinglePreview; + private static int _singlePreviewCacheDurationSeconds = -1; + private const int DefaultSinglePreviewCacheDurationSeconds = 60; + + public static PreviewMode PreviewMode + { + get + { + if (_previewMode != PreviewMode.Unknown) + return _previewMode; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.Mode"]; + if (setting.IsNullOrWhiteSpace()) + return _previewMode = DefaultPreviewMode; + if (Enum.TryParse(setting, false, out _previewMode)) + return _previewMode; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.Mode appSetting, {setting} is not a valid value. " + + "Valid values are: Vintage (default), SinglePreview."); + } + } + + public static int SinglePreviewCacheDurationSeconds + { + get + { + if (_singlePreviewCacheDurationSeconds >= 0) + return _singlePreviewCacheDurationSeconds; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.SinglePreview.CacheDurationSeconds"]; + if (setting.IsNullOrWhiteSpace()) + return _singlePreviewCacheDurationSeconds = DefaultSinglePreviewCacheDurationSeconds; + if (int.TryParse(setting, out _singlePreviewCacheDurationSeconds)) + return _singlePreviewCacheDurationSeconds; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.SinglePreview.CacheDurationSeconds appSetting, {setting} is not a valid value. " + + "Valid values are positive integers."); + } + } + + public static bool IsSinglePreview => PreviewMode == PreviewMode.SinglePreview; public XmlDocument XmlContent { get; set; } public Guid PreviewSet { get; set; } @@ -35,6 +82,8 @@ namespace umbraco.presentation.preview public void EnsureInitialized(User user, string previewSet, bool validate, Action initialize) { + if (IsSinglePreview) return; + lock (_initLock) { if (_initialized) return; @@ -53,17 +102,19 @@ namespace umbraco.presentation.preview public PreviewContent(Guid previewSet) { - ValidPreviewSet = UpdatePreviewPaths(previewSet, true); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, true); } public PreviewContent(User user, Guid previewSet, bool validate) { _userId = user.Id; - ValidPreviewSet = UpdatePreviewPaths(previewSet, validate); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, validate); } public void PrepareDocument(User user, Document documentObject, bool includeSubs) { + if (IsSinglePreview) return; + _userId = user.Id; // clone xml @@ -144,6 +195,8 @@ namespace umbraco.presentation.preview /// public bool ValidatePreviewPath() { + if (IsSinglePreview) return true; + if (!File.Exists(PreviewsetPath)) return false; @@ -154,12 +207,21 @@ namespace umbraco.presentation.preview public void LoadPreviewset() { - XmlContent = new XmlDocument(); - XmlContent.Load(PreviewsetPath); + if (IsSinglePreview) + { + XmlContent = content.Instance.PreviewXmlContent; + } + else + { + XmlContent = new XmlDocument(); + XmlContent.Load(PreviewsetPath); + } } public void SavePreviewSet() { + if (IsSinglePreview) return; + //make sure the preview folder exists first var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview)); if (!dir.Exists) @@ -211,7 +273,7 @@ namespace umbraco.presentation.preview public static void ClearPreviewCookie() { // zb-00004 #29956 : refactor cookies names & handling - if (UmbracoContext.Current.UmbracoUser != null) + if (!IsSinglePreview && UmbracoContext.Current.UmbracoUser != null) { if (StateHelper.Cookies.Preview.HasValue) {