Updates and support for re-use of CMS logic in Deploy (#14990)
* Adds additional parameter to IFileSource.GetFilesAsync allowing the caller to continue on a file not found exception. * Moved redirect tracking and creation logic out of handler into a service, allowing for re-use in Deploy. * Reverted breaking change in IFileSource by obsoleting old method and
This commit is contained in:
@@ -68,18 +68,24 @@ public interface IFileSource
|
||||
/// <param name="fileTypes">A collection of file types which can store the files.</param>
|
||||
void GetFiles(IEnumerable<StringUdi> udis, IFileTypeCollection fileTypes);
|
||||
|
||||
// TODO (V14): Remove obsolete method and default implementation for GetFilesAsync overloads.
|
||||
|
||||
/// <summary>
|
||||
/// Gets files and store them using a file store.
|
||||
/// </summary>
|
||||
/// <param name="udis">The udis of the files to get.</param>
|
||||
/// <param name="fileTypes">A collection of file types which can store the files.</param>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
[Obsolete("Please use the method overload taking all parameters. This method overload will be removed in Umbraco 14.")]
|
||||
Task GetFilesAsync(IEnumerable<StringUdi> udis, IFileTypeCollection fileTypes, CancellationToken token);
|
||||
|
||||
///// <summary>
|
||||
///// Gets the content of a file as a bytes array.
|
||||
///// </summary>
|
||||
///// <param name="Udi">A file entity identifier.</param>
|
||||
///// <returns>A byte array containing the file content.</returns>
|
||||
// byte[] GetFileBytes(StringUdi Udi);
|
||||
/// <summary>
|
||||
/// Gets files and store them using a file store.
|
||||
/// </summary>
|
||||
/// <param name="udis">The udis of the files to get.</param>
|
||||
/// <param name="fileTypes">A collection of file types which can store the files.</param>
|
||||
/// <param name="continueOnFileNotFound">A flag indicating whether to continue if a file isn't found or to stop and throw a FileNotFoundException.</param>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
Task GetFilesAsync(IEnumerable<StringUdi> udis, IFileTypeCollection fileTypes, bool continueOnFileNotFound, CancellationToken token)
|
||||
=> GetFilesAsync(udis, fileTypes, token);
|
||||
}
|
||||
|
||||
23
src/Umbraco.Core/Routing/IRedirectTracker.cs
Normal file
23
src/Umbraco.Core/Routing/IRedirectTracker.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines and records redirects for a content item following an update that may change it's public URL.
|
||||
/// </summary>
|
||||
public interface IRedirectTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the existing routes for a content item before update.
|
||||
/// </summary>
|
||||
/// <param name="entity">The content entity updated.</param>
|
||||
/// <param name="oldRoutes">The dictionary of routes for population.</param>
|
||||
void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes);
|
||||
|
||||
/// <summary>
|
||||
/// Creates appropriate redirects for the content item following an update.
|
||||
/// </summary>
|
||||
/// <param name="oldRoutes">The populated dictionary of old routes;</param>
|
||||
void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ using Umbraco.Cms.Infrastructure.Migrations.PostMigrations;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
using Umbraco.Cms.Infrastructure.Routing;
|
||||
using Umbraco.Cms.Infrastructure.Runtime;
|
||||
using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
@@ -216,6 +217,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<ICronTabParser, NCronTabParser>();
|
||||
|
||||
builder.Services.AddTransient<INodeCountService, NodeCountService>();
|
||||
|
||||
builder.Services.AddSingleton<IRedirectTracker, RedirectTracker>();
|
||||
|
||||
builder.AddInstaller();
|
||||
|
||||
// Services required to run background jobs (with out the handler)
|
||||
|
||||
125
src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
Normal file
125
src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Routing
|
||||
{
|
||||
internal class RedirectTracker : IRedirectTracker
|
||||
{
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IRedirectUrlService _redirectUrlService;
|
||||
private readonly ILogger<RedirectTracker> _logger;
|
||||
|
||||
public RedirectTracker(
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
ILocalizationService localizationService,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
ILogger<RedirectTracker> logger)
|
||||
{
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_localizationService = localizationService;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
|
||||
{
|
||||
using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
IPublishedContentCache? contentCache = reference.UmbracoContext.Content;
|
||||
IPublishedContent? entityContent = contentCache?.GetById(entity.Id);
|
||||
if (entityContent is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
|
||||
var defaultCultures = new Lazy<string[]>(() => entityContent.AncestorsOrSelf().FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty<string>());
|
||||
|
||||
// Get all language ISO codes (in case we're dealing with invariant content with variant ancestors)
|
||||
var languageIsoCodes = new Lazy<string[]>(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray());
|
||||
|
||||
foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor))
|
||||
{
|
||||
// If this entity defines specific cultures, use those instead of the default ones
|
||||
IEnumerable<string> cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value;
|
||||
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
try
|
||||
{
|
||||
var route = contentCache?.GetRouteById(publishedContent.Id, culture);
|
||||
if (IsValidRoute(route))
|
||||
{
|
||||
oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(culture))
|
||||
{
|
||||
// Retry using all languages, if this is invariant but has a variant ancestor.
|
||||
foreach (string languageIsoCode in languageIsoCodes.Value)
|
||||
{
|
||||
route = contentCache?.GetRouteById(publishedContent.Id, languageIsoCode);
|
||||
if (IsValidRoute(route))
|
||||
{
|
||||
oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not register redirects because the old route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", publishedContent.Id, culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes)
|
||||
{
|
||||
if (!oldRoutes.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
IPublishedContentCache? contentCache = reference.UmbracoContext.Content;
|
||||
if (contentCache == null)
|
||||
{
|
||||
_logger.LogWarning("Could not track redirects because there is no published content cache available on the current published snapshot.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (((int contentId, string culture), (Guid contentKey, string oldRoute)) in oldRoutes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newRoute = contentCache.GetRouteById(contentId, culture);
|
||||
if (!IsValidRoute(newRoute) || oldRoute == newRoute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_redirectUrlService.Register(oldRoute, contentKey, culture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not track redirects because the new route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", contentId, culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/");
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
@@ -30,44 +24,17 @@ public sealed class RedirectTrackingHandler :
|
||||
INotificationHandler<ContentMovedNotification>
|
||||
{
|
||||
private const string NotificationStateKey = "Umbraco.Cms.Core.Routing.RedirectTrackingHandler";
|
||||
private readonly ILogger<RedirectTrackingHandler> _logger;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRedirectUrlService _redirectUrlService;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;private readonly ILocalizationService _localizationService;
|
||||
|
||||
private readonly IOptionsMonitor<WebRoutingSettings> _webRoutingSettings;
|
||||
private readonly IRedirectTracker _redirectTracker;
|
||||
|
||||
public RedirectTrackingHandler(
|
||||
ILogger<RedirectTrackingHandler> logger,
|
||||
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
ILocalizationService localizationService){
|
||||
_logger = logger;
|
||||
IRedirectTracker redirectTracker)
|
||||
{
|
||||
_webRoutingSettings = webRoutingSettings;
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with all params")]
|
||||
public RedirectTrackingHandler(
|
||||
ILogger<RedirectTrackingHandler> logger,
|
||||
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
:this(
|
||||
logger,
|
||||
webRoutingSettings,
|
||||
publishedSnapshotAccessor,
|
||||
redirectUrlService,
|
||||
variationContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>())
|
||||
{
|
||||
|
||||
}
|
||||
_redirectTracker = redirectTracker;
|
||||
}
|
||||
|
||||
public void Handle(ContentMovedNotification notification) => CreateRedirectsForOldRoutes(notification);
|
||||
|
||||
@@ -79,150 +46,40 @@ public sealed class RedirectTrackingHandler :
|
||||
public void Handle(ContentPublishingNotification notification) =>
|
||||
StoreOldRoutes(notification.PublishedEntities, notification);
|
||||
|
||||
private static bool IsNotRoute(string? route) =>
|
||||
|
||||
// null if content not found
|
||||
// err/- if collision or anomaly or ...
|
||||
route == null || route.StartsWith("err/");
|
||||
|
||||
private void StoreOldRoutes(IEnumerable<IContent> entities, IStatefulNotification notification)
|
||||
{
|
||||
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
|
||||
// Don't let the notification handlers kick in if redirect tracking is turned off in the config.
|
||||
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OldRoutesDictionary oldRoutes = GetOldRoutes(notification);
|
||||
Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes = GetOldRoutes(notification);
|
||||
foreach (IContent entity in entities)
|
||||
{
|
||||
StoreOldRoute(entity, oldRoutes);
|
||||
_redirectTracker.StoreOldRoute(entity, oldRoutes);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRedirectsForOldRoutes(IStatefulNotification notification)
|
||||
{
|
||||
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
|
||||
// Don't let the notification handlers kick in if redirect tracking is turned off in the config.
|
||||
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OldRoutesDictionary oldRoutes = GetOldRoutes(notification);
|
||||
CreateRedirects(oldRoutes);
|
||||
Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes = GetOldRoutes(notification);
|
||||
_redirectTracker.CreateRedirects(oldRoutes);
|
||||
}
|
||||
|
||||
private OldRoutesDictionary GetOldRoutes(IStatefulNotification notification)
|
||||
private Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> GetOldRoutes(IStatefulNotification notification)
|
||||
{
|
||||
if (notification.State.ContainsKey(NotificationStateKey) == false)
|
||||
{
|
||||
notification.State[NotificationStateKey] = new OldRoutesDictionary();
|
||||
notification.State[NotificationStateKey] = new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>();
|
||||
}
|
||||
|
||||
return (OldRoutesDictionary?)notification.State[NotificationStateKey] ?? new OldRoutesDictionary();
|
||||
}
|
||||
|
||||
private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes)
|
||||
{
|
||||
if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IPublishedContentCache? contentCache = publishedSnapshot?.Content;
|
||||
IPublishedContent? entityContent = contentCache?.GetById(entity.Id);
|
||||
if (entityContent is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
|
||||
var defaultCultures = entityContent.AncestorsOrSelf().FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys
|
||||
.ToArray()
|
||||
?? Array.Empty<string>();
|
||||
|
||||
foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor))
|
||||
{
|
||||
// if this entity defines specific cultures, use those instead of the default ones
|
||||
IEnumerable<string> cultures =
|
||||
publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures;
|
||||
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
var route = contentCache?.GetRouteById(publishedContent.Id, culture);
|
||||
if (!IsNotRoute(route))
|
||||
{
|
||||
oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] = new ContentKeyAndOldRoute(publishedContent.Key, route!);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(culture))
|
||||
{
|
||||
// Retry using all languages, if this is invariant but has a variant ancestor
|
||||
var languages = _localizationService.GetAllLanguages();
|
||||
foreach (var language in languages)
|
||||
{
|
||||
route = contentCache?.GetRouteById(publishedContent.Id, language.IsoCode);
|
||||
if (!IsNotRoute(route))
|
||||
{
|
||||
oldRoutes[new ContentIdAndCulture(publishedContent.Id, language.IsoCode)] =
|
||||
new ContentKeyAndOldRoute(publishedContent.Key, route!);
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRedirects(OldRoutesDictionary oldRoutes)
|
||||
{
|
||||
if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IPublishedContentCache? contentCache = publishedSnapshot?.Content;
|
||||
|
||||
if (contentCache == null)
|
||||
{
|
||||
_logger.LogWarning("Could not track redirects because there is no current published snapshot available.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<ContentIdAndCulture, ContentKeyAndOldRoute> oldRoute in oldRoutes)
|
||||
{
|
||||
var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture);
|
||||
if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture);
|
||||
}
|
||||
}
|
||||
|
||||
private class ContentIdAndCulture : Tuple<int, string>
|
||||
{
|
||||
public ContentIdAndCulture(int contentId, string culture)
|
||||
: base(contentId, culture)
|
||||
{
|
||||
}
|
||||
|
||||
public int ContentId => Item1;
|
||||
|
||||
public string Culture => Item2;
|
||||
}
|
||||
|
||||
private class ContentKeyAndOldRoute : Tuple<Guid, string>
|
||||
{
|
||||
public ContentKeyAndOldRoute(Guid contentKey, string oldRoute)
|
||||
: base(contentKey, oldRoute)
|
||||
{
|
||||
}
|
||||
|
||||
public Guid ContentKey => Item1;
|
||||
|
||||
public string OldRoute => Item2;
|
||||
}
|
||||
|
||||
private class OldRoutesDictionary : Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>
|
||||
{
|
||||
return (Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>?)notification.State[NotificationStateKey]!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user