Determine urls at save and publish time (#17033)
* Started work on service * temp work * temp commit * Temp commit * Added more routing logic * Fixed tests * Refactor and prepare for isdraft * Work on drafts * Fixed tests * Move to enlistment to ensure caches is only updated on scope complete * Clean up and handle null cultures * Added functionality to the INavigationQueryService to get root keys * Added migration * Fixed issue with navigation * Added migration * Temp commit, move to cache refreshers. * Fixed issues * List urls * fix build * Fixed integration tests * Refactor to create new content finder instead of changing the old * rollback wrong commited line * Clean up, and use docuemnt url service for index * Fixed List endpoin * Do not use Navigation service in methods intended by management api * Fixed examine tests * Make methods virtual * Use domain from published request * Use hybrid cache from new content finder * Eliminate nucache usage * Fixed issue with delivery api and url generation * Fixed linux tests * Added hybrid cache to all integration tests
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -6,6 +8,7 @@ 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;
|
||||
@@ -14,9 +17,12 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
ContentCacheRefresher.JsonPayload>
|
||||
{
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16")]
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -25,11 +31,38 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
IDomainService domainService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
publishedSnapshotService,
|
||||
idKeyMap,
|
||||
domainService,
|
||||
eventAggregator,
|
||||
factory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>()
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IPublishedSnapshotService publishedSnapshotService,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDomainService domainService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_publishedSnapshotService = publishedSnapshotService;
|
||||
_idKeyMap = idKeyMap;
|
||||
_domainService = domainService;
|
||||
_documentUrlService = documentUrlService;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
@@ -75,7 +108,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
// By GUID Key
|
||||
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IContent, Guid?>(payload.Key));
|
||||
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
|
||||
|
||||
// remove those that are in the branch
|
||||
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
|
||||
@@ -89,6 +122,16 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
{
|
||||
idsRemoved.Add(payload.Id);
|
||||
}
|
||||
|
||||
|
||||
HandleRouting(payload);
|
||||
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
if (payload.Key.HasValue)
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Key.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (idsRemoved.Count > 0)
|
||||
@@ -129,6 +172,41 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -10,16 +10,19 @@ namespace Umbraco.Cms.Core.Cache;
|
||||
public sealed class DomainCacheRefresher : PayloadCacheRefresherBase<DomainCacheRefresherNotification, DomainCacheRefresher.JsonPayload>
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
private readonly IDomainCacheService _domainCacheService;
|
||||
|
||||
public DomainCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IPublishedSnapshotService publishedSnapshotService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory)
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IDomainCacheService domainCacheService)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_publishedSnapshotService = publishedSnapshotService;
|
||||
_domainCacheService = domainCacheService;
|
||||
}
|
||||
|
||||
#region Json
|
||||
@@ -62,6 +65,8 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase<DomainCache
|
||||
// notify
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
|
||||
_domainCacheService.Refresh(payloads);
|
||||
|
||||
// then trigger event
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,84 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class ApiPublishedContentCache : IApiPublishedContentCache
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
public ApiPublishedContentCache(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestPreviewService requestPreviewService, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings)
|
||||
public ApiPublishedContentCache(
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestCultureService requestCultureService,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IPublishedContentCache publishedContentCache)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_requestCultureService = requestCultureService;
|
||||
_documentUrlService = documentUrlService;
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
|
||||
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
|
||||
}
|
||||
|
||||
|
||||
public IPublishedContent? GetByRoute(string route)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
var isPreviewMode = _requestPreviewService.IsPreview();
|
||||
|
||||
|
||||
// Handle the nasty logic with domain document ids in front of paths.
|
||||
int? documentStartNodeId = null;
|
||||
if (route.StartsWith("/") is false)
|
||||
{
|
||||
return null;
|
||||
var index = route.IndexOf('/');
|
||||
|
||||
if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId))
|
||||
{
|
||||
documentStartNodeId = nodeId;
|
||||
route = route.Substring(index);
|
||||
}
|
||||
}
|
||||
|
||||
IPublishedContent? content = contentCache.GetByRoute(_requestPreviewService.IsPreview(), route);
|
||||
Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute(
|
||||
route,
|
||||
_requestCultureService.GetRequestedCulture(),
|
||||
documentStartNodeId,
|
||||
_requestPreviewService.IsPreview()
|
||||
);
|
||||
IPublishedContent? content = documentKey.HasValue
|
||||
? _publishedContentCache.GetById(isPreviewMode, documentKey.Value)
|
||||
: null;
|
||||
|
||||
return ContentOrNullIfDisallowed(content);
|
||||
}
|
||||
|
||||
public IPublishedContent? GetById(Guid contentId)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPublishedContent? content = contentCache.GetById(_requestPreviewService.IsPreview(), contentId);
|
||||
IPublishedContent? content = _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId);
|
||||
return ContentOrNullIfDisallowed(content);
|
||||
}
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByIds(IEnumerable<Guid> contentIds)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
{
|
||||
return Enumerable.Empty<IPublishedContent>();
|
||||
}
|
||||
|
||||
return contentIds
|
||||
.Select(contentId => contentCache.GetById(_requestPreviewService.IsPreview(), contentId))
|
||||
.Select(contentId => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId))
|
||||
.WhereNotNull()
|
||||
.Where(IsAllowedContentType)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IPublishedContentCache? GetContentCache() =>
|
||||
_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)
|
||||
? publishedSnapshot?.Content
|
||||
: null;
|
||||
|
||||
private IPublishedContent? ContentOrNullIfDisallowed(IPublishedContent? content)
|
||||
=> content != null && IsAllowedContentType(content)
|
||||
? content
|
||||
|
||||
@@ -36,7 +36,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
// devs can then modify this list on application startup
|
||||
builder.ContentFinders()
|
||||
.Append<ContentFinderByPageIdQuery>()
|
||||
.Append<ContentFinderByUrl>()
|
||||
.Append<ContentFinderByUrlNew>()
|
||||
.Append<ContentFinderByKeyPath>()
|
||||
.Append<ContentFinderByIdPath>()
|
||||
/*.Append<ContentFinderByUrlAndTemplate>() // disabled, this is an odd finder */
|
||||
@@ -47,7 +47,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes<IHealthCheckNotificationMethod>());
|
||||
builder.UrlProviders()
|
||||
.Append<AliasUrlProvider>()
|
||||
.Append<DefaultUrlProvider>();
|
||||
.Append<NewDefaultUrlProvider>();
|
||||
builder.MediaUrlProviders()
|
||||
.Append<DefaultMediaUrlProvider>();
|
||||
|
||||
|
||||
@@ -412,6 +412,10 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
|
||||
// add validation services
|
||||
Services.AddUnique<IElementSwitchValidator, ElementSwitchValidator>();
|
||||
|
||||
// Routing
|
||||
Services.AddUnique<IDocumentUrlService, DocumentUrlService>();
|
||||
Services.AddHostedService<DocumentUrlServiceInitializer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ public static class ContentBaseExtensions
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <param name="urlSegmentProviders"></param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <param name="published">Whether to get the published or draft.</param>
|
||||
/// <returns>The URL segment.</returns>
|
||||
public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null)
|
||||
public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null, bool published = true)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
@@ -30,7 +31,7 @@ public static class ContentBaseExtensions
|
||||
throw new ArgumentNullException(nameof(urlSegmentProviders));
|
||||
}
|
||||
|
||||
var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null);
|
||||
var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, published, culture)).FirstOrDefault(u => u != null);
|
||||
if (url == null)
|
||||
{
|
||||
if (_defaultUrlSegmentProvider == null)
|
||||
|
||||
@@ -296,7 +296,12 @@ public static class ContentRepositoryExtensions
|
||||
/// A value indicating whether it was possible to publish the names and values for the specified
|
||||
/// culture(s). The method may fail if required names are not set, but it does NOT validate property data
|
||||
/// </returns>
|
||||
///
|
||||
public static bool PublishCulture(this IContent content, CultureImpact? impact)
|
||||
{
|
||||
return PublishCulture(content, impact, DateTime.Now);
|
||||
}
|
||||
public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime)
|
||||
{
|
||||
if (impact == null)
|
||||
{
|
||||
@@ -323,7 +328,7 @@ public static class ContentRepositoryExtensions
|
||||
return false;
|
||||
}
|
||||
|
||||
content.SetPublishInfo(culture, name, DateTime.Now);
|
||||
content.SetPublishInfo(culture, name, publishTime);
|
||||
}
|
||||
}
|
||||
else if (impact.ImpactsOnlyInvariantCulture)
|
||||
@@ -342,7 +347,7 @@ public static class ContentRepositoryExtensions
|
||||
return false;
|
||||
}
|
||||
|
||||
content.SetPublishInfo(impact.Culture, name, DateTime.Now);
|
||||
content.SetPublishInfo(impact.Culture, name, publishTime);
|
||||
}
|
||||
|
||||
// set values
|
||||
|
||||
9
src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs
Normal file
9
src/Umbraco.Core/Models/PublishedDocumentUrlSegment.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
public class PublishedDocumentUrlSegment
|
||||
{
|
||||
public required Guid DocumentKey { get; set; }
|
||||
public required int LanguageId { get; set; }
|
||||
public required string UrlSegment { get; set; }
|
||||
public required bool IsDraft { get; set; }
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public static partial class Constants
|
||||
public const string Document = TableNamePrefix + "Document";
|
||||
public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation";
|
||||
public const string DocumentVersion = TableNamePrefix + "DocumentVersion";
|
||||
public const string DocumentUrl = TableNamePrefix + "DocumentUrl";
|
||||
public const string MediaVersion = TableNamePrefix + "MediaVersion";
|
||||
public const string ContentSchedule = TableNamePrefix + "ContentSchedule";
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
public interface IDocumentUrlRepository
|
||||
{
|
||||
void Save(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments);
|
||||
IEnumerable<PublishedDocumentUrlSegment> GetAll();
|
||||
void DeleteByDocumentKey(IEnumerable<Guid> select);
|
||||
}
|
||||
124
src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
Normal file
124
src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IContentFinder" /> that handles page nice URLs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Handles <c>/foo/bar</c> where <c>/foo/bar</c> is the nice URL of a document.</para>
|
||||
/// </remarks>
|
||||
public class ContentFinderByUrlNew : IContentFinder
|
||||
{
|
||||
private readonly ILogger<ContentFinderByUrlNew> _logger;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
|
||||
/// </summary>
|
||||
public ContentFinderByUrlNew(
|
||||
ILogger<ContentFinderByUrlNew> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IPublishedContentCache publishedContentCache)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_documentUrlService = documentUrlService;
|
||||
UmbracoContextAccessor =
|
||||
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IUmbracoContextAccessor" />
|
||||
/// </summary>
|
||||
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find and assign an Umbraco document to a <c>PublishedRequest</c>.
|
||||
/// </summary>
|
||||
/// <param name="frequest">The <c>PublishedRequest</c>.</param>
|
||||
/// <returns>A value indicating whether an Umbraco document was found and assigned.</returns>
|
||||
public virtual Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
|
||||
{
|
||||
if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? _))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
string route;
|
||||
if (frequest.Domain != null)
|
||||
{
|
||||
route = frequest.Domain.ContentId +
|
||||
DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded);
|
||||
}
|
||||
else
|
||||
{
|
||||
route = frequest.AbsolutePathDecoded;
|
||||
}
|
||||
|
||||
IPublishedContent? node = FindContent(frequest, route);
|
||||
return Task.FromResult(node != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an Umbraco document for a <c>PublishedRequest</c> and a route.
|
||||
/// </summary>
|
||||
/// <returns>The document node, or null.</returns>
|
||||
protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route)
|
||||
{
|
||||
if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (docreq == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(docreq));
|
||||
}
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Test route {Route}", route);
|
||||
}
|
||||
|
||||
var documentKey = _documentUrlService.GetDocumentKeyByRoute(
|
||||
docreq.Domain is null ? route : route.Substring(docreq.Domain.ContentId.ToString().Length),
|
||||
docreq.Culture,
|
||||
docreq.Domain?.ContentId,
|
||||
umbracoContext.InPreviewMode
|
||||
);
|
||||
|
||||
IPublishedContent? node = null;
|
||||
if (documentKey.HasValue)
|
||||
{
|
||||
node = _publishedContentCache.GetById(umbracoContext.InPreviewMode, documentKey.Value);
|
||||
//node = umbracoContext.Content?.GetById(umbracoContext.InPreviewMode, documentKey.Value);
|
||||
}
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
docreq.SetPublishedContent(node);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got content, id={NodeId}", node.Id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("No match.");
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
284
src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
Normal file
284
src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides urls.
|
||||
/// </summary>
|
||||
public class NewDefaultUrlProvider : IUrlProvider
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private readonly IDomainCache _domainCache;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private readonly ILocalizedTextService? _localizedTextService;
|
||||
private readonly ILogger<DefaultUrlProvider> _logger;
|
||||
private readonly ISiteDomainMapper _siteDomainMapper;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
private readonly UriUtility _uriUtility;
|
||||
private RequestHandlerSettings _requestSettings;
|
||||
|
||||
public NewDefaultUrlProvider(
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
ILogger<DefaultUrlProvider> logger,
|
||||
ISiteDomainMapper siteDomainMapper,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
UriUtility uriUtility,
|
||||
ILocalizationService localizationService,
|
||||
IPublishedContentCache publishedContentCache,
|
||||
IDomainCache domainCache,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDocumentUrlService documentUrlService)
|
||||
{
|
||||
_requestSettings = requestSettings.CurrentValue;
|
||||
_logger = logger;
|
||||
_siteDomainMapper = siteDomainMapper;
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
_uriUtility = uriUtility;
|
||||
_localizationService = localizationService;
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_domainCache = domainCache;
|
||||
_idKeyMap = idKeyMap;
|
||||
_documentUrlService = documentUrlService;
|
||||
|
||||
requestSettings.OnChange(x => _requestSettings = x);
|
||||
}
|
||||
|
||||
#region GetOtherUrls
|
||||
|
||||
/// <summary>
|
||||
/// Gets the other URLs of a published content.
|
||||
/// </summary>
|
||||
/// <param name="id">The published content id.</param>
|
||||
/// <param name="current">The current absolute URL.</param>
|
||||
/// <returns>The other URLs for the published content.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Other URLs are those that <c>GetUrl</c> would not return in the current context, but would be valid
|
||||
/// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
|
||||
{
|
||||
var keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
||||
|
||||
if (keyAttempt.Success is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var key = keyAttempt.Result;
|
||||
|
||||
IPublishedContent? node = _publishedContentCache.GetById(key);
|
||||
if (node == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// look for domains, walking up the tree
|
||||
IPublishedContent? n = node;
|
||||
IEnumerable<DomainAndUri>? domainUris =
|
||||
DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current, false);
|
||||
|
||||
// n is null at root
|
||||
while (domainUris == null && n != null)
|
||||
{
|
||||
n = n.Parent; // move to parent node
|
||||
domainUris = n == null
|
||||
? null
|
||||
: DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current);
|
||||
}
|
||||
|
||||
// no domains = exit
|
||||
if (domainUris == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (DomainAndUri d in domainUris)
|
||||
{
|
||||
var culture = d.Culture;
|
||||
|
||||
// although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok
|
||||
var route = GetLegacyRouteFormatById(key, culture);
|
||||
if (route == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned)
|
||||
var pos = route.IndexOf('/');
|
||||
var path = pos == 0 ? route : route.Substring(pos);
|
||||
|
||||
var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path));
|
||||
uri = _uriUtility.UriFromUmbraco(uri, _requestSettings);
|
||||
yield return UrlInfo.Url(uri.ToString(), culture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the legacy route format by id
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// When no domain is set the route can be something like /child/grandchild
|
||||
/// When a domain is set, the route can be something like 1234/grandchild
|
||||
/// </remarks>
|
||||
|
||||
private string GetLegacyRouteFormatById(Guid key, string? culture)
|
||||
{
|
||||
|
||||
return _documentUrlService.GetLegacyRouteFormat(key, culture, _umbracoContextAccessor.GetRequiredUmbracoContext().InPreviewMode);
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUrl
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
|
||||
{
|
||||
if (!current.IsAbsoluteUri)
|
||||
{
|
||||
throw new ArgumentException("Current URL must be absolute.", nameof(current));
|
||||
}
|
||||
|
||||
|
||||
// will not use cache if previewing
|
||||
var route = GetLegacyRouteFormatById(content.Key, culture);
|
||||
|
||||
return GetUrlFromRoute(route, content.Id, current, mode, culture);
|
||||
}
|
||||
|
||||
internal UrlInfo? GetUrlFromRoute(
|
||||
string? route,
|
||||
int id,
|
||||
Uri current,
|
||||
UrlMode mode,
|
||||
string? culture)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(route))
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.",
|
||||
id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract domainUri and path
|
||||
// route is /<path> or <domainRootId>/<path>
|
||||
var pos = route.IndexOf('/');
|
||||
var path = pos == 0 ? route : route[pos..];
|
||||
DomainAndUri? domainUri = pos == 0
|
||||
? null
|
||||
: DomainUtilities.DomainForNode(
|
||||
_domainCache,
|
||||
_siteDomainMapper,
|
||||
int.Parse(route[..pos], CultureInfo.InvariantCulture),
|
||||
current,
|
||||
culture);
|
||||
|
||||
var defaultCulture = _localizationService.GetDefaultLanguageIsoCode();
|
||||
if (domainUri is not null || string.IsNullOrEmpty(culture) ||
|
||||
culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var url = AssembleUrl(domainUri, path, current, mode).ToString();
|
||||
return UrlInfo.Url(url, culture);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utilities
|
||||
|
||||
private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode)
|
||||
{
|
||||
Uri uri;
|
||||
|
||||
// ignore vdir at that point, UriFromUmbraco will do it
|
||||
// no domain was found
|
||||
if (domainUri == null)
|
||||
{
|
||||
if (current == null)
|
||||
{
|
||||
mode = UrlMode.Relative; // best we can do
|
||||
}
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case UrlMode.Absolute:
|
||||
uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path);
|
||||
break;
|
||||
case UrlMode.Relative:
|
||||
case UrlMode.Auto:
|
||||
uri = new Uri(path, UriKind.Relative);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(mode));
|
||||
}
|
||||
}
|
||||
|
||||
// a domain was found
|
||||
else
|
||||
{
|
||||
if (mode == UrlMode.Auto)
|
||||
{
|
||||
// this check is a little tricky, we can't just compare domains
|
||||
if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) ==
|
||||
current.GetLeftPart(UriPartial.Authority))
|
||||
{
|
||||
mode = UrlMode.Relative;
|
||||
}
|
||||
else
|
||||
{
|
||||
mode = UrlMode.Absolute;
|
||||
}
|
||||
}
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case UrlMode.Absolute:
|
||||
uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path));
|
||||
break;
|
||||
case UrlMode.Relative:
|
||||
uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(mode));
|
||||
}
|
||||
}
|
||||
|
||||
// UriFromUmbraco will handle vdir
|
||||
// meaning it will add vdir into domain URLs too!
|
||||
return _uriUtility.UriFromUmbraco(uri, _requestSettings);
|
||||
}
|
||||
|
||||
private string CombinePaths(string path1, string path2)
|
||||
{
|
||||
var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2;
|
||||
return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class PublishedRouter : IPublishedRouter
|
||||
private readonly IContentLastChanceFinder _contentLastChanceFinder;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IDomainCache _domainCache;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly ILogger<PublishedRouter> _logger;
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
@@ -50,7 +51,8 @@ public class PublishedRouter : IPublishedRouter
|
||||
IFileService fileService,
|
||||
IContentTypeService contentTypeService,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IEventAggregator eventAggregator)
|
||||
IEventAggregator eventAggregator,
|
||||
IDomainCache domainCache)
|
||||
{
|
||||
_webRoutingSettings = webRoutingSettings.CurrentValue ??
|
||||
throw new ArgumentNullException(nameof(webRoutingSettings));
|
||||
@@ -68,6 +70,7 @@ public class PublishedRouter : IPublishedRouter
|
||||
_contentTypeService = contentTypeService;
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
_eventAggregator = eventAggregator;
|
||||
_domainCache = domainCache;
|
||||
webRoutingSettings.OnChange(x => _webRoutingSettings = x);
|
||||
}
|
||||
|
||||
@@ -404,11 +407,10 @@ public class PublishedRouter : IPublishedRouter
|
||||
}
|
||||
|
||||
var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null;
|
||||
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
|
||||
Domain? domain =
|
||||
DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId);
|
||||
DomainUtilities.FindWildcardDomainInPath(_domainCache.GetAll(true), nodePath, rootNodeId);
|
||||
|
||||
|
||||
// always has a contentId and a culture
|
||||
if (domain != null)
|
||||
{
|
||||
request.SetCulture(domain.Culture);
|
||||
|
||||
@@ -133,10 +133,10 @@ namespace Umbraco.Cms.Core.Routing
|
||||
public string GetUrlFromRoute(int id, string? route, string? culture)
|
||||
{
|
||||
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
|
||||
DefaultUrlProvider? provider = _urlProviders.OfType<DefaultUrlProvider>().FirstOrDefault();
|
||||
NewDefaultUrlProvider? provider = _urlProviders.OfType<NewDefaultUrlProvider>().FirstOrDefault();
|
||||
var url = provider == null
|
||||
? route // what else?
|
||||
: provider.GetUrlFromRoute(route, umbracoContext, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text;
|
||||
: provider.GetUrlFromRoute(route, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text;
|
||||
return url ?? "#";
|
||||
}
|
||||
|
||||
|
||||
@@ -1060,7 +1060,14 @@ public class ContentService : RepositoryService, IContentService
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule",
|
||||
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
|
||||
() =>
|
||||
{
|
||||
_documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key);
|
||||
if (content.Trashed)
|
||||
{
|
||||
_documentNavigationManagementService.MoveToBin(content.Key);
|
||||
}
|
||||
});
|
||||
|
||||
if (contentSchedule != null)
|
||||
{
|
||||
@@ -1129,7 +1136,14 @@ public class ContentService : RepositoryService, IContentService
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Save",
|
||||
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
|
||||
() =>
|
||||
{
|
||||
_documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key);
|
||||
if (content.Trashed)
|
||||
{
|
||||
_documentNavigationManagementService.MoveToBin(content.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.Notifications.Publish(
|
||||
@@ -1227,9 +1241,10 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
// publish the culture(s)
|
||||
// we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
|
||||
var publishTime = DateTime.Now;
|
||||
foreach (CultureImpact? impact in impacts)
|
||||
{
|
||||
content.PublishCulture(impact);
|
||||
content.PublishCulture(impact, publishTime);
|
||||
}
|
||||
|
||||
// Change state to publishing
|
||||
@@ -1866,7 +1881,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
// publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
IProperty[]? invalidProperties = null;
|
||||
CultureImpact impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
var tryPublish = d.PublishCulture(impact) &&
|
||||
var tryPublish = d.PublishCulture(impact, date) &&
|
||||
_propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
{
|
||||
@@ -1943,17 +1958,19 @@ public class ContentService : RepositoryService, IContentService
|
||||
{
|
||||
// variant content type - publish specified cultures
|
||||
// invariant content type - publish only the invariant culture
|
||||
|
||||
var publishTime = DateTime.Now;
|
||||
if (content.ContentType.VariesByCulture())
|
||||
{
|
||||
return culturesToPublish.All(culture =>
|
||||
{
|
||||
CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
|
||||
return content.PublishCulture(impact) &&
|
||||
return content.PublishCulture(impact, publishTime) &&
|
||||
_propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
|
||||
});
|
||||
}
|
||||
|
||||
return content.PublishCulture(_cultureImpactFactory.ImpactInvariant())
|
||||
return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime)
|
||||
&& _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant());
|
||||
}
|
||||
|
||||
@@ -3179,7 +3196,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
.ToArray();
|
||||
|
||||
// publish the culture(s)
|
||||
if (!impactsToPublish.All(content.PublishCulture))
|
||||
var publishTime = DateTime.Now;
|
||||
if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime)))
|
||||
{
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
}
|
||||
|
||||
670
src/Umbraco.Core/Services/DocumentUrlService.cs
Normal file
670
src/Umbraco.Core/Services/DocumentUrlService.cs
Normal file
@@ -0,0 +1,670 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public class DocumentUrlService : IDocumentUrlService
|
||||
{
|
||||
private const string RebuildKey = "UmbracoUrlGeneration";
|
||||
|
||||
private readonly ILogger<DocumentUrlService> _logger;
|
||||
private readonly IDocumentUrlRepository _documentUrlRepository;
|
||||
private readonly IDocumentRepository _documentRepository;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly UrlSegmentProviderCollection _urlSegmentProviderCollection;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
private readonly IDomainService _domainService;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PublishedDocumentUrlSegment> _cache = new();
|
||||
private bool _isInitialized = false;
|
||||
|
||||
public DocumentUrlService(
|
||||
ILogger<DocumentUrlService> logger,
|
||||
IDocumentUrlRepository documentUrlRepository,
|
||||
IDocumentRepository documentRepository,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
UrlSegmentProviderCollection urlSegmentProviderCollection,
|
||||
IContentService contentService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ILanguageService languageService,
|
||||
IKeyValueService keyValueService,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService,
|
||||
IDomainService domainService)
|
||||
{
|
||||
_logger = logger;
|
||||
_documentUrlRepository = documentUrlRepository;
|
||||
_documentRepository = documentRepository;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_urlSegmentProviderCollection = urlSegmentProviderCollection;
|
||||
_contentService = contentService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_languageService = languageService;
|
||||
_keyValueService = keyValueService;
|
||||
_idKeyMap = idKeyMap;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
_domainService = domainService;
|
||||
}
|
||||
|
||||
public async Task InitAsync(bool forceEmpty, CancellationToken cancellationToken)
|
||||
{
|
||||
if (forceEmpty)
|
||||
{
|
||||
// We have this use case when umbraco is installing, we know there is no routes. And we can execute the normal logic because the connection string is missing.
|
||||
_isInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
if (await ShouldRebuildUrlsAsync())
|
||||
{
|
||||
_logger.LogInformation("Rebuilding all urls.");
|
||||
await RebuildAllUrlsAsync();
|
||||
}
|
||||
|
||||
IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments = _documentUrlRepository.GetAll();
|
||||
|
||||
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
|
||||
var languageIdToIsoCode = languages.ToDictionary(x => x.Id, x => x.IsoCode);
|
||||
foreach (PublishedDocumentUrlSegment publishedDocumentUrlSegment in publishedDocumentUrlSegments)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (languageIdToIsoCode.TryGetValue(publishedDocumentUrlSegment.LanguageId, out var isoCode))
|
||||
{
|
||||
UpdateCache(_coreScopeProvider.Context!, publishedDocumentUrlSegment, isoCode);
|
||||
}
|
||||
}
|
||||
_isInitialized = true;
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private void UpdateCache(IScopeContext scopeContext, PublishedDocumentUrlSegment publishedDocumentUrlSegment, string isoCode)
|
||||
{
|
||||
var cacheKey = CreateCacheKey(publishedDocumentUrlSegment.DocumentKey, isoCode, publishedDocumentUrlSegment.IsDraft);
|
||||
|
||||
scopeContext.Enlist("UpdateCache_" + cacheKey, () =>
|
||||
{
|
||||
PublishedDocumentUrlSegment? existingValue = null;
|
||||
_cache.TryGetValue(cacheKey, out existingValue);
|
||||
|
||||
if (existingValue is null)
|
||||
{
|
||||
if (_cache.TryAdd(cacheKey, publishedDocumentUrlSegment) is false)
|
||||
{
|
||||
_logger.LogError("Could not add the document url cache.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_cache.TryUpdate(cacheKey, publishedDocumentUrlSegment, existingValue) is false)
|
||||
{
|
||||
_logger.LogError("Could not update the document url cache.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode)
|
||||
{
|
||||
var cacheKeyDraft = CreateCacheKey(documentKey, isoCode, true);
|
||||
|
||||
scopeContext.Enlist("RemoveFromCache_" + cacheKeyDraft, () =>
|
||||
{
|
||||
if (_cache.TryRemove(cacheKeyDraft, out _) is false)
|
||||
{
|
||||
_logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
var cacheKeyPublished = CreateCacheKey(documentKey, isoCode, false);
|
||||
|
||||
scopeContext.Enlist("RemoveFromCache_" + cacheKeyPublished, () =>
|
||||
{
|
||||
if (_cache.TryRemove(cacheKeyPublished, out _) is false)
|
||||
{
|
||||
_logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public async Task RebuildAllUrlsAsync()
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
|
||||
IEnumerable<IContent> documents = _documentRepository.GetMany(Array.Empty<Guid>());
|
||||
|
||||
await CreateOrUpdateUrlSegmentsAsync(documents);
|
||||
|
||||
_keyValueService.SetValue(RebuildKey, GetCurrentRebuildValue());
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
public Task<bool> ShouldRebuildUrlsAsync()
|
||||
{
|
||||
var persistedValue = GetPersistedRebuildValue();
|
||||
var currentValue = GetCurrentRebuildValue();
|
||||
|
||||
return Task.FromResult(string.Equals(persistedValue, currentValue) is false);
|
||||
}
|
||||
|
||||
private string GetCurrentRebuildValue()
|
||||
{
|
||||
return string.Join("|", _urlSegmentProviderCollection.Select(x => x.GetType().Name));
|
||||
}
|
||||
|
||||
private string? GetPersistedRebuildValue() => _keyValueService.GetValue(RebuildKey);
|
||||
|
||||
public string? GetUrlSegment(Guid documentKey, string culture, bool isDraft)
|
||||
{
|
||||
ThrowIfNotInitialized();
|
||||
var cacheKey = CreateCacheKey(documentKey, culture, isDraft);
|
||||
|
||||
_cache.TryGetValue(cacheKey, out PublishedDocumentUrlSegment? urlSegment);
|
||||
|
||||
return urlSegment?.UrlSegment;
|
||||
}
|
||||
|
||||
private void ThrowIfNotInitialized()
|
||||
{
|
||||
if (_isInitialized is false)
|
||||
{
|
||||
throw new InvalidOperationException("The service needs to be initialized before it can be used.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateUrlSegmentsAsync(IEnumerable<IContent> documents)
|
||||
{
|
||||
if(documents.Any() is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
|
||||
var toSave = new List<PublishedDocumentUrlSegment>();
|
||||
var toDelete = new List<Guid>();
|
||||
var allCultures = documents.SelectMany(x => x.AvailableCultures ).Distinct();
|
||||
|
||||
var languages = await _languageService.GetMultipleAsync(allCultures);
|
||||
var languageDictionary = languages.ToDictionary(x=>x.IsoCode);
|
||||
|
||||
foreach (IContent document in documents)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_logger.LogTrace("Rebuilding urls for document with key {DocumentKey}", document.Key);
|
||||
}
|
||||
|
||||
if (document.AvailableCultures.Any())
|
||||
{
|
||||
foreach (var culture in document.AvailableCultures)
|
||||
{
|
||||
var language = languageDictionary[culture];
|
||||
|
||||
HandleCaching(_coreScopeProvider.Context!, document, culture, language, toDelete, toSave);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var language = await _languageService.GetDefaultLanguageAsync();
|
||||
|
||||
HandleCaching(_coreScopeProvider.Context!, document, null, language!, toDelete, toSave);
|
||||
}
|
||||
}
|
||||
|
||||
if(toSave.Any())
|
||||
{
|
||||
_documentUrlRepository.Save(toSave);
|
||||
}
|
||||
|
||||
if(toDelete.Any())
|
||||
{
|
||||
_documentUrlRepository.DeleteByDocumentKey(toDelete);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private void HandleCaching(IScopeContext scopeContext, IContent document, string? culture, ILanguage language, List<Guid> toDelete, List<PublishedDocumentUrlSegment> toSave)
|
||||
{
|
||||
var models = GenerateModels(document, culture, language);
|
||||
|
||||
foreach (PublishedDocumentUrlSegment model in models)
|
||||
{
|
||||
if (document.Published is false && model.IsDraft is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (document.Trashed)
|
||||
{
|
||||
toDelete.Add(model.DocumentKey);
|
||||
RemoveFromCache(scopeContext, model.DocumentKey, language.IsoCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
toSave.Add(model);
|
||||
UpdateCache(scopeContext, model, language.IsoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<PublishedDocumentUrlSegment> GenerateModels(IContent document, string? culture, ILanguage language)
|
||||
{
|
||||
var publishedUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, true);
|
||||
if(publishedUrlSegment.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.LogWarning("No published url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new PublishedDocumentUrlSegment()
|
||||
{
|
||||
DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = publishedUrlSegment, IsDraft = false
|
||||
};
|
||||
}
|
||||
|
||||
var draftUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, false);
|
||||
|
||||
if(draftUrlSegment.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.LogWarning("No draft url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new PublishedDocumentUrlSegment()
|
||||
{
|
||||
DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = draftUrlSegment, IsDraft = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeys)
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
|
||||
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
|
||||
|
||||
foreach (ILanguage language in languages)
|
||||
{
|
||||
foreach (Guid documentKey in documentKeys)
|
||||
{
|
||||
RemoveFromCache(_coreScopeProvider.Context!, documentKey, language.IsoCode);
|
||||
}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
public Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft)
|
||||
{
|
||||
var urlSegments = route.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// We need to translate legacy int ids to guid keys.
|
||||
Guid? runnerKey = GetStartNodeKey(documentStartNodeId);
|
||||
var hideTopLevelNodeFromPath = _globalSettings.HideTopLevelNodeFromPath;
|
||||
|
||||
culture ??= _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult();
|
||||
|
||||
if (!_globalSettings.ForceCombineUrlPathLeftToRight
|
||||
&& CultureInfo.GetCultureInfo(culture).TextInfo.IsRightToLeft)
|
||||
{
|
||||
urlSegments = urlSegments.Reverse().ToArray();
|
||||
}
|
||||
|
||||
// If a domain is assigned to this route, we need to follow the url segments
|
||||
if (runnerKey.HasValue)
|
||||
{
|
||||
// If there is no url segments it means the domain root has been requested
|
||||
if (urlSegments.Length == 0)
|
||||
{
|
||||
return runnerKey.Value;
|
||||
}
|
||||
|
||||
// Otherwise we have to find the child with that segment anc follow that
|
||||
foreach (var urlSegment in urlSegments)
|
||||
{
|
||||
//Get the children of the runnerKey and find the child (if any) with the correct url segment
|
||||
var childKeys = GetChildKeys(runnerKey.Value);
|
||||
|
||||
runnerKey = GetChildWithUrlSegment(childKeys, urlSegment, culture, isDraft);
|
||||
|
||||
if (runnerKey is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return runnerKey;
|
||||
}
|
||||
// If there is no parts, it means it is a root (and no assigned domain)
|
||||
if(urlSegments.Length == 0)
|
||||
{
|
||||
// // if we do not hide the top level and no domain was found, it maens there is no content.
|
||||
// // TODO we can remove this to keep consistency with the old routing, but it seems incorrect to allow that.
|
||||
// if (hideTopLevelNodeFromPath is false)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return GetTopMostRootKey();
|
||||
}
|
||||
|
||||
// Otherwise we have to find the root items (or child of the first root when hideTopLevelNodeFromPath is true) and follow the url segments in them to get to correct document key
|
||||
for (var index = 0; index < urlSegments.Length; index++)
|
||||
{
|
||||
var urlSegment = urlSegments[index];
|
||||
IEnumerable<Guid> runnerKeys;
|
||||
if (index == 0)
|
||||
{
|
||||
runnerKeys = GetKeysInRoot(hideTopLevelNodeFromPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (runnerKey is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runnerKeys = GetChildKeys(runnerKey.Value);
|
||||
}
|
||||
|
||||
runnerKey = GetChildWithUrlSegment(runnerKeys, urlSegment, culture, isDraft);
|
||||
}
|
||||
|
||||
return runnerKey;
|
||||
}
|
||||
|
||||
public string GetLegacyRouteFormat(Guid docuemntKey, string? culture, bool isDraft)
|
||||
{
|
||||
var documentIdAttempt = _idKeyMap.GetIdForKey(docuemntKey, UmbracoObjectTypes.Document);
|
||||
|
||||
if(documentIdAttempt.Success is false)
|
||||
{
|
||||
return "#";
|
||||
}
|
||||
|
||||
if (_documentNavigationQueryService.TryGetAncestorsOrSelfKeys(docuemntKey,
|
||||
out IEnumerable<Guid> ancestorsOrSelfKeys) is false)
|
||||
{
|
||||
return "#";
|
||||
}
|
||||
|
||||
var cultureOrDefault = culture ?? _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult();
|
||||
|
||||
Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
|
||||
IDictionary<Guid, IDomain?> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, ancestorKey =>
|
||||
{
|
||||
IEnumerable<IDomain> domains = _domainService.GetAssignedDomainsAsync(ancestorKey, false).GetAwaiter().GetResult();
|
||||
return domains.FirstOrDefault(x=>x.LanguageIsoCode == cultureOrDefault);
|
||||
});
|
||||
|
||||
var urlSegments = new List<string>();
|
||||
|
||||
IDomain? foundDomain = null;
|
||||
|
||||
foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray)
|
||||
{
|
||||
if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out IDomain? domain))
|
||||
{
|
||||
if (domain is not null)
|
||||
{
|
||||
foundDomain = domain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(CreateCacheKey(ancestorOrSelfKey, cultureOrDefault, isDraft), out PublishedDocumentUrlSegment? publishedDocumentUrlSegment))
|
||||
{
|
||||
urlSegments.Add(publishedDocumentUrlSegment.UrlSegment);
|
||||
}
|
||||
|
||||
if (foundDomain is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDomain is not null)
|
||||
{
|
||||
//we found a domain, and not to construct the route in the funny legacy way
|
||||
return foundDomain.RootContentId + "/" + string.Join("/", urlSegments);
|
||||
}
|
||||
|
||||
var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last();
|
||||
return GetFullUrl(isRootFirstItem, urlSegments, null);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey)
|
||||
{
|
||||
var result = new List<UrlInfo>();
|
||||
|
||||
var documentIdAttempt = _idKeyMap.GetIdForKey(contentKey, UmbracoObjectTypes.Document);
|
||||
|
||||
if(documentIdAttempt.Success is false)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
IEnumerable<Guid> ancestorsOrSelfKeys = contentKey.Yield()
|
||||
.Concat(_contentService.GetAncestors(documentIdAttempt.Result).Select(x => x.Key).Reverse());
|
||||
|
||||
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
|
||||
IEnumerable<string> cultures = languages.Select(x=>x.IsoCode);
|
||||
|
||||
|
||||
Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
|
||||
Dictionary<Guid, Task<Dictionary<string, IDomain>>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, async ancestorKey =>
|
||||
{
|
||||
IEnumerable<IDomain> domains = await _domainService.GetAssignedDomainsAsync(ancestorKey, false);
|
||||
return domains.ToDictionary(x => x.LanguageIsoCode!);
|
||||
});
|
||||
|
||||
var urlSegments = new List<string>();
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
IDomain? foundDomain = null;
|
||||
|
||||
foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray)
|
||||
{
|
||||
if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task<Dictionary<string, IDomain>>? domainDictionaryTask))
|
||||
{
|
||||
var domainDictionary = await domainDictionaryTask;
|
||||
if (domainDictionary.TryGetValue(culture, out IDomain? domain))
|
||||
{
|
||||
foundDomain = domain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(CreateCacheKey(ancestorOrSelfKey, culture, false), out PublishedDocumentUrlSegment? publishedDocumentUrlSegment))
|
||||
{
|
||||
urlSegments.Add(publishedDocumentUrlSegment.UrlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last();
|
||||
result.Add(new UrlInfo(
|
||||
text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain),
|
||||
isUrl: true,
|
||||
culture: culture
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetFullUrl(bool isRootFirstItem, List<string> reversedUrlSegments, IDomain? foundDomain)
|
||||
{
|
||||
var urlSegments = new List<string>(reversedUrlSegments);
|
||||
urlSegments.Reverse();
|
||||
|
||||
if (foundDomain is not null)
|
||||
{
|
||||
return foundDomain.DomainName + string.Join('/', urlSegments);
|
||||
}
|
||||
|
||||
return '/' + string.Join('/', urlSegments.Skip(_globalSettings.HideTopLevelNodeFromPath && isRootFirstItem ? 1 : 0));
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key)
|
||||
{
|
||||
var id = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document).Result;
|
||||
IContent item = _contentService.GetById(id)!;
|
||||
IEnumerable<IContent> descendants = _contentService.GetPagedDescendants(id, 0, int.MaxValue, out _);
|
||||
|
||||
await CreateOrUpdateUrlSegmentsAsync(new List<IContent>(descendants)
|
||||
{
|
||||
item
|
||||
});
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateUrlSegmentsAsync(Guid key)
|
||||
{
|
||||
IContent? content = _contentService.GetById(key);
|
||||
|
||||
if (content is not null)
|
||||
{
|
||||
await CreateOrUpdateUrlSegmentsAsync(content.Yield());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//TODO test cases:
|
||||
// - Find the root, when a domain is set
|
||||
// - Find a nested child, when a domain is set
|
||||
|
||||
// - Find the root when no domain is set and hideTopLevelNodeFromPath is true
|
||||
// - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is true
|
||||
// - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is true
|
||||
// - Find the root when no domain is set and hideTopLevelNodeFromPath is false
|
||||
// - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is false
|
||||
// - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is false
|
||||
|
||||
// - All of the above when having Constants.Conventions.Content.UrlName set to a value
|
||||
|
||||
private IEnumerable<Guid> GetKeysInRoot(bool addFirstLevelChildren)
|
||||
{
|
||||
//TODO replace with something more performand - Should be possible with navigationservice..
|
||||
IEnumerable<Guid> rootKeys = _contentService.GetRootContent().Select(x=>x.Key).ToArray();
|
||||
|
||||
foreach (Guid rootKey in rootKeys)
|
||||
{
|
||||
yield return rootKey;
|
||||
}
|
||||
|
||||
if (addFirstLevelChildren)
|
||||
{
|
||||
foreach (Guid rootKey in rootKeys)
|
||||
{
|
||||
IEnumerable<Guid> childKeys = GetChildKeys(rootKey);
|
||||
|
||||
foreach (Guid childKey in childKeys)
|
||||
{
|
||||
yield return childKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Guid? GetChildWithUrlSegment(IEnumerable<Guid> childKeys, string urlSegment, string culture, bool isDraft)
|
||||
{
|
||||
foreach (Guid childKey in childKeys)
|
||||
{
|
||||
var childUrlSegment = GetUrlSegment(childKey, culture, isDraft);
|
||||
|
||||
if (string.Equals(childUrlSegment, urlSegment))
|
||||
{
|
||||
return childKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children based on the latest published version of the content. (No aware of things in this scope).
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The key of the document to get children from.</param>
|
||||
/// <returns>The keys of all the children of the document.</returns>
|
||||
private IEnumerable<Guid> GetChildKeys(Guid documentKey)
|
||||
{
|
||||
if(_documentNavigationQueryService.TryGetChildrenKeys(documentKey, out IEnumerable<Guid> childrenKeys))
|
||||
{
|
||||
return childrenKeys;
|
||||
}
|
||||
|
||||
return Enumerable.Empty<Guid>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top most root key.
|
||||
/// </summary>
|
||||
/// <returns>The top most root key.</returns>
|
||||
private Guid? GetTopMostRootKey()
|
||||
{
|
||||
if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys))
|
||||
{
|
||||
return rootKeys.FirstOrDefault();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCacheKey(Guid documentKey, string culture, bool isDraft) => $"{documentKey}|{culture}|{isDraft}";
|
||||
|
||||
private Guid? GetStartNodeKey(int? documentStartNodeId)
|
||||
{
|
||||
if (documentStartNodeId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Attempt<Guid> attempt = _idKeyMap.GetKeyForId(documentStartNodeId.Value, UmbracoObjectTypes.Document);
|
||||
return attempt.Success ? attempt.Result : null;
|
||||
}
|
||||
|
||||
}
|
||||
53
src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs
Normal file
53
src/Umbraco.Core/Services/DocumentUrlServiceInitializer.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public class DocumentUrlServiceInitializer : IHostedLifecycleService
|
||||
{
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
public DocumentUrlServiceInitializer(IDocumentUrlService documentUrlService, IRuntimeState runtimeState)
|
||||
{
|
||||
_documentUrlService = documentUrlService;
|
||||
_runtimeState = runtimeState;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_runtimeState.Level == RuntimeLevel.Upgrade)
|
||||
{
|
||||
//Special case on the first upgrade, as the database is not ready yet.
|
||||
return;
|
||||
}
|
||||
|
||||
await _documentUrlService.InitAsync(
|
||||
_runtimeState.Level <= RuntimeLevel.Install,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StoppingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
36
src/Umbraco.Core/Services/IDocumentUrlService.cs
Normal file
36
src/Umbraco.Core/Services/IDocumentUrlService.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Umbraco.Cms.Core.Media.EmbedProviders;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IDocumentUrlService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service and ensure the content in the database is correct with the current configuration.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task InitAsync(bool forceEmpty, CancellationToken cancellationToken);
|
||||
|
||||
Task RebuildAllUrlsAsync();
|
||||
/// <summary>
|
||||
/// Gets the Url from a document key, culture and segment. Preview urls are returned if isPreview is true.
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The key of the document.</param>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
|
||||
/// <returns>The url of the document.</returns>
|
||||
string? GetUrlSegment(Guid documentKey, string culture, bool isDraft);
|
||||
|
||||
Task CreateOrUpdateUrlSegmentsAsync(IEnumerable<IContent> documents);
|
||||
|
||||
Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeys);
|
||||
|
||||
Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft);
|
||||
Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey);
|
||||
Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key);
|
||||
Task CreateOrUpdateUrlSegmentsAsync(Guid key);
|
||||
string GetLegacyRouteFormat(Guid key, string? culture, bool isDraft);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,7 +16,32 @@ public interface INavigationQueryService
|
||||
|
||||
bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
|
||||
|
||||
bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable<Guid> descendantsOrSelfKeys)
|
||||
{
|
||||
if(TryGetDescendantsKeys(childKey, out var descendantsKeys))
|
||||
{
|
||||
descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys);
|
||||
return true;
|
||||
}
|
||||
|
||||
descendantsOrSelfKeys = Array.Empty<Guid>();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool TryGetAncestorsKeys(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
|
||||
|
||||
bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable<Guid> ancestorsOrSelfKeys)
|
||||
{
|
||||
if(TryGetAncestorsKeys(childKey, out var ancestorsKeys))
|
||||
{
|
||||
ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys);
|
||||
return true;
|
||||
}
|
||||
|
||||
ancestorsOrSelfKeys = Array.Empty<Guid>();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryGetSiblingsKeys(Guid key, out IEnumerable<Guid> siblingsKeys);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,6 +14,19 @@ public interface IRecycleBinNavigationQueryService
|
||||
|
||||
bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
|
||||
|
||||
bool TryGetDescendantsKeysOrSelfKeysInBin(Guid childKey, out IEnumerable<Guid> descendantsOrSelfKeys)
|
||||
{
|
||||
if(TryGetDescendantsKeysInBin(childKey, out var descendantsKeys))
|
||||
{
|
||||
descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys);
|
||||
return true;
|
||||
}
|
||||
|
||||
descendantsOrSelfKeys = Array.Empty<Guid>();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
|
||||
|
||||
bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable<Guid> siblingsKeys);
|
||||
|
||||
@@ -12,21 +12,25 @@ public class DefaultUrlSegmentProvider : IUrlSegmentProvider
|
||||
|
||||
public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper;
|
||||
|
||||
|
||||
public virtual string? GetUrlSegment(IContentBase content, bool published, string? culture = null) =>
|
||||
GetUrlSegmentSource(content, culture, published)?.ToUrlSegment(_shortStringHelper, culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segment for a specified content and culture.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The URL segment.</returns>
|
||||
public string? GetUrlSegment(IContentBase content, string? culture = null) =>
|
||||
GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture);
|
||||
public virtual string? GetUrlSegment(IContentBase content, string? culture = null) =>
|
||||
GetUrlSegmentSource(content, culture, true)?.ToUrlSegment(_shortStringHelper, culture);
|
||||
|
||||
private static string? GetUrlSegmentSource(IContentBase content, string? culture)
|
||||
private static string? GetUrlSegmentSource(IContentBase content, string? culture, bool published)
|
||||
{
|
||||
string? source = null;
|
||||
if (content.HasProperty(Constants.Conventions.Content.UrlName))
|
||||
{
|
||||
source = (content.GetValue<string>(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim();
|
||||
source = (content.GetValue<string>(Constants.Conventions.Content.UrlName, culture, published: published) ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
|
||||
@@ -20,6 +20,7 @@ public interface IUrlSegmentProvider
|
||||
/// URL per culture.
|
||||
/// </remarks>
|
||||
string? GetUrlSegment(IContentBase content, string? culture = null);
|
||||
string? GetUrlSegment(IContentBase content, bool published, string? culture = null) => GetUrlSegment(content, culture);
|
||||
|
||||
// TODO: For the 301 tracking, we need to add another extended interface to this so that
|
||||
// the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing.
|
||||
|
||||
Reference in New Issue
Block a user