<commit_before>﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.XPath;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
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
    {
        private readonly ContentStore.Snapshot _snapshot;
        private readonly ICacheProvider _snapshotCache;
        private readonly ICacheProvider _elementsCache;
        private readonly DomainHelper _domainHelper;
        private readonly IGlobalSettings _globalSettings;
        private readonly ILocalizationService _localizationService;

        #region Constructor

        public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, ICacheProvider snapshotCache, ICacheProvider elementsCache, DomainHelper domainHelper, IGlobalSettings globalSettings, ILocalizationService localizationService)
            : base(previewDefault)
        {
            _snapshot = snapshot;
            _snapshotCache = snapshotCache;
            _elementsCache = elementsCache;
            _domainHelper = domainHelper;
            _globalSettings = globalSettings;
            _localizationService = localizationService;
        }

        private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath;

        #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)
        {
            return GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
        }

        public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
        {
            if (route == null) throw new ArgumentNullException(nameof(route));

            var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing ? _elementsCache : _snapshotCache;
            var key = CacheKeys.ContentCacheContentByRoute(route, preview, culture);
            return cache.GetCacheItem<IPublishedContent>(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture));
        }

        private IPublishedContent GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode, string culture)
        {
            hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings

            // 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 then /en part of the domain is basically ignored here...
                content = GetById(preview, startNodeId);
                content = FollowRoute(content, parts, 0, culture);
            }
            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.GetUrlSegment(culture) == parts[0])
                    : GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]);
                content = FollowRoute(content, parts, 1, culture);
            }

            // 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.GetUrlSegment(culture) == parts[0]);
            }

            return content;
        }

        public string GetRouteById(int contentId, string culture = null)
        {
            return GetRouteById(PreviewDefault, contentId, culture);
        }

        public string GetRouteById(bool preview, int contentId, string culture = null)
        {
            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));
        }

        private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode, string culture)
        {
            var node = GetById(preview, contentId);
            if (node == null)
                return null;

            hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings

            // 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.GetUrlSegment(culture);
            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;

                pathParts.Add(urlSegment);

                // move to parent node
                n = n.Parent;
                urlSegment = n.GetUrlSegment(culture);
                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;

            // 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; <commit_msg>adding missing null check after navigating to parent.<commit_after>﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.XPath;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
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
    {
        private readonly ContentStore.Snapshot _snapshot;
        private readonly ICacheProvider _snapshotCache;
        private readonly ICacheProvider _elementsCache;
        private readonly DomainHelper _domainHelper;
        private readonly IGlobalSettings _globalSettings;
        private readonly ILocalizationService _localizationService;

        #region Constructor

        public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, ICacheProvider snapshotCache, ICacheProvider elementsCache, DomainHelper domainHelper, IGlobalSettings globalSettings, ILocalizationService localizationService)
            : base(previewDefault)
        {
            _snapshot = snapshot;
            _snapshotCache = snapshotCache;
            _elementsCache = elementsCache;
            _domainHelper = domainHelper;
            _globalSettings = globalSettings;
            _localizationService = localizationService;
        }

        private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath;

        #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)
        {
            return GetByRoute(PreviewDefault, route, hideTopLevelNode, culture);
        }

        public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
        {
            if (route == null) throw new ArgumentNullException(nameof(route));

            var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing ? _elementsCache : _snapshotCache;
            var key = CacheKeys.ContentCacheContentByRoute(route, preview, culture);
            return cache.GetCacheItem<IPublishedContent>(key, () => GetByRouteInternal(preview, route, hideTopLevelNode, culture));
        }

        private IPublishedContent GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode, string culture)
        {
            hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings

            // 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 then /en part of the domain is basically ignored here...
                content = GetById(preview, startNodeId);
                content = FollowRoute(content, parts, 0, culture);
            }
            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.GetUrlSegment(culture) == parts[0])
                    : GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]);
                content = FollowRoute(content, parts, 1, culture);
            }

            // 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.GetUrlSegment(culture) == parts[0]);
            }

            return content;
        }

        public string GetRouteById(int contentId, string culture = null)
        {
            return GetRouteById(PreviewDefault, contentId, culture);
        }

        public string GetRouteById(bool preview, int contentId, string culture = null)
        {
            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));
        }

        private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode, string culture)
        {
            var node = GetById(preview, contentId);
            if (node == null)
                return null;

            hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings

            // 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.GetUrlSegment(culture);
            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;

                pathParts.Add(urlSegment);

                // move to parent node
                n = n.Parent;
                if (n != null)
                    urlSegment = n.GetUrlSegment(culture);

                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;

            // 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; 
{'fraction_non_alphanumeric': 0.08237547892720307, 'fraction_numerical': 0.002224694104560623, 'mean_word_length': 2.2275628240925407, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2399231', 'n_tokens_mistral': 4104, 'n_tokens_neox': 3936, 'n_words': 1292}