using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache; public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { private readonly IDomainService _domainService; private readonly IDomainCacheService _domainCacheService; private readonly IDocumentUrlService _documentUrlService; private readonly IDocumentNavigationQueryService _documentNavigationQueryService; private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private readonly IContentService _contentService; private readonly IIdKeyMap _idKeyMap; public ContentCacheRefresher( AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IDomainService domainService, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory, IDocumentUrlService documentUrlService, IDomainCacheService domainCacheService, IDocumentNavigationQueryService documentNavigationQueryService, IDocumentNavigationManagementService documentNavigationManagementService, IContentService contentService) : base(appCaches, serializer, eventAggregator, factory) { _idKeyMap = idKeyMap; _domainService = domainService; _domainCacheService = domainCacheService; _documentUrlService = documentUrlService; _documentNavigationQueryService = documentNavigationQueryService; _documentNavigationManagementService = documentNavigationManagementService; _contentService = contentService; } #region Indirect public static void RefreshContentTypes(AppCaches appCaches) { // we could try to have a mechanism to notify the PublishedCachesService // and figure out whether published items were modified or not... keep it // simple for now, just clear the whole thing appCaches.ClearPartialViewCache(); appCaches.IsolatedCaches.ClearCache(); appCaches.IsolatedCaches.ClearCache(); } #endregion #region Define public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); public override Guid RefresherUniqueId => UniqueId; public override string Name => "ContentCacheRefresher"; #endregion #region Refresher public override void Refresh(JsonPayload[] payloads) { AppCaches.RuntimeCache.ClearOfType(); AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); var idsRemoved = new HashSet(); IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); foreach (JsonPayload payload in payloads.Where(x => x.Id != default)) { // By INT Id isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); // By GUID Key isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) { var pathid = "," + payload.Id + ","; isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); } // if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) { idsRemoved.Add(payload.Id); } HandleRouting(payload); HandleNavigation(payload); _idKeyMap.ClearCache(payload.Id); if (payload.Key.HasValue) { _idKeyMap.ClearCache(payload.Key.Value); } } if (idsRemoved.Count > 0) { var assignedDomains = _domainService.GetAll(true) ?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); if (assignedDomains?.Count > 0) { // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the // DomainCacheRefresher? ClearAllIsolatedCacheByEntityType(); // note: must do what's above FIRST else the repositories still have the old cached // content and when the PublishedCachesService is notified of changes it does not see // the new content... // notify _domainCacheService.Refresh(assignedDomains .Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); } } base.Refresh(payloads); } private void HandleNavigation(JsonPayload payload) { if (payload.Key is null) { return; } if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { _documentNavigationManagementService.MoveToBin(payload.Key.Value); _documentNavigationManagementService.RemoveFromBin(payload.Key.Value); } if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { _documentNavigationManagementService.RebuildAsync(); _documentNavigationManagementService.RebuildBinAsync(); } if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode)) { IContent? content = _contentService.GetById(payload.Id); if (content is null) { return; } HandleNavigationForSingleContent(content); } if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { IContent? content = _contentService.GetById(payload.Id); if (content is null) { return; } IEnumerable descendants = _contentService.GetPagedDescendants(content.Id, 0, int.MaxValue, out _); foreach (IContent descendant in content.Yield().Concat(descendants)) { HandleNavigationForSingleContent(descendant); } } } private void HandleNavigationForSingleContent(IContent content) { // First creation if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key) is false) { _documentNavigationManagementService.Add(content.Key, GetParentKey(content)); if (content.Trashed) { // If created as trashed, move to bin _documentNavigationManagementService.MoveToBin(content.Key); } } else if (ExistsInNavigation(content.Key) && ExistsInNavigationBin(content.Key) is false) { if (content.Trashed) { // It must have been trashed _documentNavigationManagementService.MoveToBin(content.Key); } else { // It must have been saved. Check if parent is different if (_documentNavigationQueryService.TryGetParentKey(content.Key, out var oldParentKey)) { Guid? newParentKey = GetParentKey(content); if (oldParentKey != newParentKey) { _documentNavigationManagementService.Move(content.Key, newParentKey); } } } } else if (ExistsInNavigation(content.Key) is false && ExistsInNavigationBin(content.Key)) { if (content.Trashed is false) { // It must have been restored _documentNavigationManagementService.RestoreFromBin(content.Key, GetParentKey(content)); } } } private Guid? GetParentKey(IContent content) => (content.ParentId == -1) ? null : _idKeyMap.GetKeyForId(content.ParentId, UmbracoObjectTypes.Document).Result; private bool ExistsInNavigation(Guid contentKey) => _documentNavigationQueryService.TryGetParentKey(contentKey, out _); private bool ExistsInNavigationBin(Guid contentKey) => _documentNavigationQueryService.TryGetParentKeyInBin(contentKey, out _); private void HandleRouting(JsonPayload payload) { if(payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; //Note the we need to clear the navigation service as the last thing if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out var descendantsOrSelfKeys)) { _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult(); }else if(_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin)) { _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult(); } } if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { _documentUrlService.RebuildAllUrlsAsync().GetAwaiter().GetResult(); //TODO make async } if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; _documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult(); } if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; _documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult(); } } // these events should never trigger // everything should be PAYLOAD/JSON public override void RefreshAll() => throw new NotSupportedException(); public override void Refresh(int id) => throw new NotSupportedException(); public override void Refresh(Guid id) => throw new NotSupportedException(); public override void Remove(int id) => throw new NotSupportedException(); #endregion #region Json // TODO (V14): Change into a record public class JsonPayload { public JsonPayload() { } [Obsolete("Use the default constructor and property initializers.")] public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) { Id = id; Key = key; ChangeTypes = changeTypes; } public int Id { get; init; } public Guid? Key { get; init; } public TreeChangeTypes ChangeTypes { get; init; } public bool Blueprint { get; init; } } #endregion }