Files
Umbraco-CMS/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs

401 lines
16 KiB
C#
Raw Normal View History

2016-05-27 14:26:28 +02:00
using System;
2018-03-30 14:00:44 +02:00
using System.Collections;
2016-05-27 14:26:28 +02:00
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.XPath;
using Umbraco.Core;
using Umbraco.Core.Cache;
2018-03-30 14:00:44 +02:00
using Umbraco.Core.Composing;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Configuration;
2018-03-30 14:00:44 +02:00
using Umbraco.Core.Models;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Models.PublishedContent;
2018-03-30 14:00:44 +02:00
using Umbraco.Core.Services;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Xml;
using Umbraco.Core.Xml.XPath;
using Umbraco.Web.PublishedCache.NuCache.Navigable;
using Umbraco.Web.Routing;
namespace Umbraco.Web.PublishedCache.NuCache
{
internal class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable
2016-05-27 14:26:28 +02:00
{
2017-07-12 14:09:31 +02:00
private readonly ContentStore.Snapshot _snapshot;
2019-01-17 11:01:23 +01:00
private readonly IAppCache _snapshotCache;
private readonly IAppCache _elementsCache;
2016-05-27 14:26:28 +02:00
private readonly DomainHelper _domainHelper;
private readonly IGlobalSettings _globalSettings;
private readonly ILocalizationService _localizationService;
2016-05-27 14:26:28 +02:00
#region Constructor
// TODO: figure this out
2018-12-17 18:52:43 +01:00
// after the current snapshot has been resync-ed
// it's too late for UmbracoContext which has captured previewDefault and stuff into these ctor vars
// but, no, UmbracoContext returns snapshot.Content which comes from elements SO a resync should create a new cache
2019-01-17 11:01:23 +01:00
public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, DomainHelper domainHelper, IGlobalSettings globalSettings, ILocalizationService localizationService)
2016-05-27 14:26:28 +02:00
: base(previewDefault)
{
_snapshot = snapshot;
_snapshotCache = snapshotCache;
2017-10-31 12:48:24 +01:00
_elementsCache = elementsCache;
2016-05-27 14:26:28 +02:00
_domainHelper = domainHelper;
_globalSettings = globalSettings;
_localizationService = localizationService;
2016-05-27 14:26:28 +02:00
}
private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath;
2017-07-12 14:09:31 +02:00
2016-05-27 14:26:28 +02:00
#endregion
#region Routes
// routes can be
// "/"
// "123/"
// "/path/to/node"
// "123/path/to/node"
// at the moment we try our best to be backward compatible, but really,
// should get rid of hideTopLevelNode and other oddities entirely, eventually
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string culture = null)
2016-05-27 14:26:28 +02:00
{
return GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
2016-05-27 14:26:28 +02:00
}
public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
2016-05-27 14:26:28 +02:00
{
2017-07-12 14:09:31 +02:00
if (route == null) throw new ArgumentNullException(nameof(route));
2016-05-27 14:26:28 +02:00
2017-10-31 12:48:24 +01:00
var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing ? _elementsCache : _snapshotCache;
2018-04-24 14:39:52 +10:00
var key = CacheKeys.ContentCacheContentByRoute(route, preview, culture);
return cache.GetCacheItem<IPublishedContent>(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture));
2016-05-27 14:26:28 +02:00
}
private IPublishedContent GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode, string culture)
2016-05-27 14:26:28 +02:00
{
2017-07-12 14:09:31 +02:00
hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings
2016-05-27 14:26:28 +02:00
// the route always needs to be lower case because we only store the urlName attribute in lower case
route = route.ToLowerInvariant();
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
IPublishedContent content;
if (startNodeId > 0)
{
// if in a domain then start with the root node of the domain
// and follow the path
// note: if domain has a path (eg example.com/en) which is not recommended anymore
// then /en part of the domain is basically ignored here...
2016-05-27 14:26:28 +02:00
content = GetById(preview, startNodeId);
content = FollowRoute(content, parts, 0, culture);
2016-05-27 14:26:28 +02:00
}
else if (parts.Length == 0)
{
// if not in a domain, and path is empty - what is the default page?
// let's say it is the first one in the tree, if any -- order by sortOrder
content = GetAtRoot(preview).FirstOrDefault();
}
else
{
// if not in a domain...
// hideTopLevelNode = support legacy stuff, look for /*/path/to/node
// else normal, look for /path/to/node
content = hideTopLevelNode.Value
? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.UrlSegment(culture) == parts[0])
: GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]);
content = FollowRoute(content, parts, 1, culture);
2016-05-27 14:26:28 +02:00
}
// if hideTopLevelNodePath is true then for url /foo we looked for /*/foo
// but maybe that was the url of a non-default top-level node, so we also
// have to look for /foo (see note in ApplyHideTopLevelNodeFromPath).
if (content == null && hideTopLevelNode.Value && parts.Length == 1)
{
content = GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]);
2016-05-27 14:26:28 +02:00
}
return content;
}
public string GetRouteById(int contentId, string culture = null)
2016-05-27 14:26:28 +02:00
{
return GetRouteById(PreviewDefault, contentId, culture);
2016-05-27 14:26:28 +02:00
}
public string GetRouteById(bool preview, int contentId, string culture = null)
2016-05-27 14:26:28 +02:00
{
2017-10-31 12:48:24 +01:00
var cache = (preview == false || PublishedSnapshotService.FullCacheWhenPreviewing) ? _elementsCache : _snapshotCache;
var key = CacheKeys.ContentCacheRouteByContent(contentId, preview, culture);
return cache.GetCacheItem<string>(key, () => GetRouteByIdInternal(preview, contentId, null, culture));
2016-05-27 14:26:28 +02:00
}
private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode, string culture)
2016-05-27 14:26:28 +02:00
{
var node = GetById(preview, contentId);
if (node == null)
return null;
2017-07-12 14:09:31 +02:00
hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings
2016-05-27 14:26:28 +02:00
// walk up from that node until we hit a node with a domain,
// or we reach the content root, collecting urls in the way
var pathParts = new List<string>();
var n = node;
var urlSegment = n.UrlSegment(culture);
2016-05-27 14:26:28 +02:00
var hasDomains = _domainHelper.NodeHasDomains(n.Id);
while (hasDomains == false && n != null) // n is null at root
{
// no segment indicates this is not published when this is a variant
if (urlSegment.IsNullOrWhiteSpace()) return null;
2018-04-28 16:34:43 +02:00
pathParts.Add(urlSegment);
2016-05-27 14:26:28 +02:00
// move to parent node
n = n.Parent;
if (n != null)
urlSegment = n.UrlSegment(culture);
2016-05-27 14:26:28 +02:00
hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id);
}
// at this point this will be the urlSegment of the root, no segment indicates this is not published when this is a variant
if (urlSegment.IsNullOrWhiteSpace()) return null;
2016-05-27 14:26:28 +02:00
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (hasDomains == false && hideTopLevelNode.Value)
ApplyHideTopLevelNodeFromPath(node, pathParts, preview);
// assemble the route
pathParts.Reverse();
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
//prefix the root node id containing the domain if it exists (this is a standard way of creating route paths)
//and is done so that we know the ID of the domain node for the path
var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path;
2016-05-27 14:26:28 +02:00
return route;
}
private IPublishedContent FollowRoute(IPublishedContent content, IReadOnlyList<string> parts, int start, string culture)
2016-05-27 14:26:28 +02:00
{
var i = start;
while (content != null && i < parts.Count)
{
var part = parts[i++];
content = content.Children.FirstOrDefault(x =>
{
var urlSegment = x.UrlSegment(culture);
2018-04-28 16:34:43 +02:00
return urlSegment == part;
});
2016-05-27 14:26:28 +02:00
}
return content;
}
private void ApplyHideTopLevelNodeFromPath(IPublishedContent content, IList<string> segments, bool preview)
{
// in theory if hideTopLevelNodeFromPath is true, then there should be only one
// top-level node, or else domains should be assigned. but for backward compatibility
// we add this check - we look for the document matching "/" and if it's not us, then
// we do not hide the top level path
// it has to be taken care of in GetByRoute too so if
2016-05-30 19:54:36 +02:00
// "/foo" fails (looking for "/*/foo") we try also "/foo".
2016-05-27 14:26:28 +02:00
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (content.Parent == null)
{
var rootNode = GetByRoute(preview, "/", true);
if (rootNode == null)
throw new Exception("Failed to get node at /.");
if (rootNode.Id == content.Id) // remove only if we're the default node
segments.RemoveAt(segments.Count - 1);
}
else
{
segments.RemoveAt(segments.Count - 1);
}
}
#endregion
#region Get, Has
public override IPublishedContent GetById(bool preview, int contentId)
{
var node = _snapshot.Get(contentId);
return GetNodePublishedContent(node, preview);
2016-05-27 14:26:28 +02:00
}
public override IPublishedContent GetById(bool preview, Guid contentId)
{
var node = _snapshot.Get(contentId);
return GetNodePublishedContent(node, preview);
}
2016-05-27 14:26:28 +02:00
public override bool HasById(bool preview, int contentId)
{
var n = _snapshot.Get(contentId);
if (n == null) return false;
2019-01-28 14:15:47 +01:00
return preview || n.PublishedModel != null;
2016-05-27 14:26:28 +02:00
}
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview)
{
2017-10-31 12:48:24 +01:00
if (PublishedSnapshotService.CacheContentCacheRoots == false)
2016-05-27 14:26:28 +02:00
return GetAtRootNoCache(preview);
2017-10-31 12:48:24 +01:00
var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing
? _elementsCache
: _snapshotCache;
2016-05-27 14:26:28 +02:00
if (cache == null)
return GetAtRootNoCache(preview);
// note: ToArray is important here, we want to cache the result, not the function!
2019-01-17 11:01:23 +01:00
return (IEnumerable<IPublishedContent>)cache.Get(
2016-05-27 14:26:28 +02:00
CacheKeys.ContentCacheRoots(preview),
() => GetAtRootNoCache(preview).ToArray());
}
private IEnumerable<IPublishedContent> GetAtRootNoCache(bool preview)
{
var c = _snapshot.GetAtRoot();
// both .Draft and .Published cannot be null at the same time
return c.Select(n => GetNodePublishedContent(n, preview)).WhereNotNull().OrderBy(x => x.SortOrder);
}
private static IPublishedContent GetNodePublishedContent(ContentNode node, bool preview)
{
if (node == null)
return null;
// both .Draft and .Published cannot be null at the same time
return preview
2019-01-28 14:15:47 +01:00
? node.DraftModel ?? GetPublishedContentAsDraft(node.PublishedModel)
: node.PublishedModel;
2016-05-27 14:26:28 +02:00
}
// gets a published content as a previewing draft, if preview is true
// this is for published content when previewing
private static IPublishedContent GetPublishedContentAsDraft(IPublishedContent content /*, bool preview*/)
2016-05-27 14:26:28 +02:00
{
if (content == null /*|| preview == false*/) return null; //content;
2016-05-30 19:54:36 +02:00
2016-05-27 14:26:28 +02:00
// an object in the cache is either an IPublishedContentOrMedia,
// or a model inheriting from PublishedContentExtended - in which
// case we need to unwrap to get to the original IPublishedContentOrMedia.
var inner = PublishedContent.UnwrapIPublishedContent(content);
return inner.AsDraft();
2016-05-27 14:26:28 +02:00
}
public override bool HasContent(bool preview)
{
return preview
? _snapshot.IsEmpty == false
2019-01-28 14:15:47 +01:00
: _snapshot.GetAtRoot().Any(x => x.PublishedModel != null);
2016-05-27 14:26:28 +02:00
}
#endregion
#region XPath
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
return GetSingleByXPath(iterator);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
return GetSingleByXPath(iterator);
}
private static IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
{
if (iterator.MoveNext() == false) return null;
var xnav = iterator.Current as NavigableNavigator;
2017-07-12 14:09:31 +02:00
var xcontent = xnav?.UnderlyingObject as NavigableContent;
return xcontent?.InnerContent;
2016-05-27 14:26:28 +02:00
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
return GetByXPath(iterator);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
var navigator = CreateNavigator(preview);
var iterator = navigator.Select(xpath, vars);
return GetByXPath(iterator);
}
private static IEnumerable<IPublishedContent> GetByXPath(XPathNodeIterator iterator)
{
while (iterator.MoveNext())
{
var xnav = iterator.Current as NavigableNavigator;
2017-07-12 14:09:31 +02:00
var xcontent = xnav?.UnderlyingObject as NavigableContent;
2016-05-27 14:26:28 +02:00
if (xcontent == null) continue;
yield return xcontent.InnerContent;
}
}
public override XPathNavigator CreateNavigator(bool preview)
{
var source = new Source(this, preview);
var navigator = new NavigableNavigator(source);
return navigator;
}
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
{
var source = new Source(this, preview);
var navigator = new NavigableNavigator(source);
return navigator.CloneWithNewRoot(id, 0);
}
#endregion
#region Content types
2019-04-15 13:04:14 +02:00
public override IPublishedContentType GetContentType(int id)
2016-05-27 14:26:28 +02:00
{
return _snapshot.GetContentType(id);
}
2019-04-15 13:04:14 +02:00
public override IPublishedContentType GetContentType(string alias)
2016-05-27 14:26:28 +02:00
{
return _snapshot.GetContentType(alias);
}
#endregion
#region IDisposable
public void Dispose()
{
_snapshot.Dispose();
}
#endregion
}
}