Refactor preview
This commit is contained in:
@@ -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<IContent>();
|
||||
ClearAllIsolatedCacheByEntityType<PublicAccessEntry>();
|
||||
DistributedCache.Instance.ClearDomainCacheOnCurrentServer();
|
||||
content.Instance.ClearPreviewXmlContent();
|
||||
base.RefreshAll();
|
||||
}
|
||||
|
||||
@@ -87,6 +89,9 @@ namespace Umbraco.Web.Cache
|
||||
ClearRepositoryCacheItemById(id);
|
||||
ClearAllIsolatedCacheByEntityType<PublicAccessEntry>();
|
||||
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<PublicAccessEntry>();
|
||||
DistributedCache.Instance.ClearDomainCacheOnCurrentServer();
|
||||
content.Instance.ClearPreviewXmlContent(id);
|
||||
base.Remove(id);
|
||||
}
|
||||
|
||||
@@ -106,6 +112,9 @@ namespace Umbraco.Web.Cache
|
||||
ClearRepositoryCacheItemById(instance.Id);
|
||||
ClearAllIsolatedCacheByEntityType<PublicAccessEntry>();
|
||||
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<PublicAccessEntry>();
|
||||
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();
|
||||
|
||||
@@ -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, () =>
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -474,6 +478,8 @@ namespace umbraco
|
||||
safeXml.AcceptChanges();
|
||||
}
|
||||
}
|
||||
|
||||
ClearPreviewXmlContent(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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("<!ATTLIST {0} id ID #REQUIRED>", 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<XmlDocument>(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<XmlDocument>(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<XmlDocument>(PreviewCacheKey);
|
||||
if (xml == null) return;
|
||||
|
||||
// sanity checks
|
||||
if (id != docNode.AttributeValue<int>("id"))
|
||||
throw new ArgumentException("Values of id and docNode/@id are different.");
|
||||
if (parentId != docNode.AttributeValue<int>("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<int>("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<int>("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<int>("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<content>("Loading preview content from database...");
|
||||
var xml = ApplicationContext.Current.Services.ContentService.BuildPreviewXmlCache();
|
||||
LogHelper.Debug<content>("Done loading preview content");
|
||||
return xml;
|
||||
}
|
||||
catch (Exception ee)
|
||||
{
|
||||
LogHelper.Error<content>("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<XmlDocument>(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true,
|
||||
removedCallback: (key, removed, reason) => LogHelper.Debug<content>($"Removed preview xml from cache ({reason})"));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PreviewMode>.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
|
||||
/// <returns></returns>
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user