Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/Umbraco.Core/Deploy/IFileSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 23 additions & 0 deletions src/Umbraco.Core/Routing/IRedirectTracker.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
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;
Expand Down Expand Up @@ -215,6 +216,9 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
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)
Expand Down
125 changes: 125 additions & 0 deletions src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
Original file line number Diff line number Diff line change
@@ -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/");
}
}
Loading