V10: fix build warnings infrastructure (#12369)
* Run code cleanup * Run dotnet format * Start manual fixes * Manual fixing of warnings * Fix nullability in columnalias * Fix tests * Fix up after merge * Start updating after review * Update editorconfig to contain new static & const rules * Fix up editorconfig to not contain duplicate rules * Fix up static member names * Fix up according to review * Update src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Macros/MacroTagParser.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Macros/MacroTagParser.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Macros/MacroTagParser.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Fix [..] to substring * Fix after merge with 10/dev * Fox ContentValueSetValidator.cs * Update LoggerConfigExtensions Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk> Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Examine;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -10,131 +7,132 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Infrastructure;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IContentFinder" /> that runs the legacy 404 logic.
|
||||
/// </summary>
|
||||
public class ContentFinderByConfigured404 : IContentLastChanceFinder
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IExamineManager _examineManager;
|
||||
private readonly ILogger<ContentFinderByConfigured404> _logger;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private ContentSettings _contentSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IContentFinder"/> that runs the legacy 404 logic.
|
||||
/// Initializes a new instance of the <see cref="ContentFinderByConfigured404" /> class.
|
||||
/// </summary>
|
||||
public class ContentFinderByConfigured404 : IContentLastChanceFinder
|
||||
public ContentFinderByConfigured404(
|
||||
ILogger<ContentFinderByConfigured404> logger,
|
||||
IEntityService entityService,
|
||||
IOptionsMonitor<ContentSettings> contentSettings,
|
||||
IExamineManager examineManager,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
private readonly ILogger<ContentFinderByConfigured404> _logger;
|
||||
private readonly IEntityService _entityService;
|
||||
private ContentSettings _contentSettings;
|
||||
private readonly IExamineManager _examineManager;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
_logger = logger;
|
||||
_entityService = entityService;
|
||||
_contentSettings = contentSettings.CurrentValue;
|
||||
_examineManager = examineManager;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentFinderByConfigured404"/> class.
|
||||
/// </summary>
|
||||
public ContentFinderByConfigured404(
|
||||
ILogger<ContentFinderByConfigured404> logger,
|
||||
IEntityService entityService,
|
||||
IOptionsMonitor<ContentSettings> contentSettings,
|
||||
IExamineManager examineManager,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IUmbracoContextAccessor umbracoContextAccessor)
|
||||
contentSettings.OnChange(x => _contentSettings = x);
|
||||
}
|
||||
|
||||
/// <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 Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
|
||||
{
|
||||
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
|
||||
{
|
||||
_logger = logger;
|
||||
_entityService = entityService;
|
||||
_contentSettings = contentSettings.CurrentValue;
|
||||
_examineManager = examineManager;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
|
||||
contentSettings.OnChange(x => _contentSettings = x);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
/// <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 async Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Looking for a page to handle 404.");
|
||||
}
|
||||
_logger.LogDebug("Looking for a page to handle 404.");
|
||||
}
|
||||
|
||||
int? domainContentId = null;
|
||||
int? domainContentId = null;
|
||||
|
||||
// try to find a culture as best as we can
|
||||
string? errorCulture = CultureInfo.CurrentUICulture.Name;
|
||||
if (frequest.Domain != null)
|
||||
// try to find a culture as best as we can
|
||||
var errorCulture = CultureInfo.CurrentUICulture.Name;
|
||||
if (frequest.Domain != null)
|
||||
{
|
||||
errorCulture = frequest.Domain.Culture;
|
||||
domainContentId = frequest.Domain.ContentId;
|
||||
}
|
||||
else
|
||||
{
|
||||
var route = frequest.AbsolutePathDecoded;
|
||||
var pos = route.LastIndexOf('/');
|
||||
IPublishedContent? node = null;
|
||||
while (pos > 1)
|
||||
{
|
||||
errorCulture = frequest.Domain.Culture;
|
||||
domainContentId = frequest.Domain.ContentId;
|
||||
}
|
||||
else
|
||||
{
|
||||
var route = frequest.AbsolutePathDecoded;
|
||||
var pos = route.LastIndexOf('/');
|
||||
IPublishedContent? node = null;
|
||||
while (pos > 1)
|
||||
{
|
||||
route = route.Substring(0, pos);
|
||||
node = umbracoContext.Content?.GetByRoute(route, culture: frequest?.Culture);
|
||||
if (node != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
pos = route.LastIndexOf('/');
|
||||
}
|
||||
|
||||
route = route.Substring(0, pos);
|
||||
node = umbracoContext.Content?.GetByRoute(route, culture: frequest?.Culture);
|
||||
if (node != null)
|
||||
{
|
||||
Domain? d = DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), node.Path, null);
|
||||
if (d != null)
|
||||
{
|
||||
errorCulture = d.Culture;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
pos = route.LastIndexOf('/');
|
||||
}
|
||||
|
||||
var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId(
|
||||
_contentSettings.Error404Collection.ToArray(),
|
||||
_entityService,
|
||||
new PublishedContentQuery(umbracoContext.PublishedSnapshot, _variationContextAccessor, _examineManager),
|
||||
errorCulture,
|
||||
domainContentId);
|
||||
|
||||
IPublishedContent? content = null;
|
||||
|
||||
if (error404.HasValue)
|
||||
if (node != null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
Domain? d = DomainUtilities.FindWildcardDomainInPath(
|
||||
umbracoContext.PublishedSnapshot.Domains?.GetAll(true), node.Path, null);
|
||||
if (d != null)
|
||||
{
|
||||
_logger.LogDebug("Got id={ErrorNodeId}.", error404.Value);
|
||||
errorCulture = d.Culture;
|
||||
}
|
||||
content = umbracoContext.Content?.GetById(error404.Value);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(content == null
|
||||
}
|
||||
}
|
||||
|
||||
var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId(
|
||||
_contentSettings.Error404Collection.ToArray(),
|
||||
_entityService,
|
||||
new PublishedContentQuery(umbracoContext.PublishedSnapshot, _variationContextAccessor, _examineManager),
|
||||
errorCulture,
|
||||
domainContentId);
|
||||
|
||||
IPublishedContent? content = null;
|
||||
|
||||
if (error404.HasValue)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got id={ErrorNodeId}.", error404.Value);
|
||||
}
|
||||
|
||||
content = umbracoContext.Content?.GetById(error404.Value);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(content == null
|
||||
? "Could not find content with that id."
|
||||
: "Found corresponding content.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got nothing.");
|
||||
}
|
||||
}
|
||||
|
||||
frequest?
|
||||
.SetPublishedContent(content)
|
||||
.SetIs404();
|
||||
|
||||
return content != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got nothing.");
|
||||
}
|
||||
}
|
||||
|
||||
frequest?
|
||||
.SetPublishedContent(content)
|
||||
.SetIs404();
|
||||
|
||||
return Task.FromResult(content != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -9,99 +7,99 @@ using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Xml;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to determine the node to display when content is not found based on the configured error404 elements in umbracoSettings.config
|
||||
/// </summary>
|
||||
internal class NotFoundHandlerHelper
|
||||
{
|
||||
internal static int? GetCurrentNotFoundPageId(
|
||||
ContentErrorPage[] error404Collection,
|
||||
IEntityService entityService,
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
string? errorCulture,
|
||||
int? domainContentId)
|
||||
{
|
||||
if (error404Collection.Length > 1)
|
||||
{
|
||||
// test if a 404 page exists with current culture thread
|
||||
ContentErrorPage? cultureErr = error404Collection.FirstOrDefault(x => x.Culture.InvariantEquals(errorCulture))
|
||||
?? error404Collection.FirstOrDefault(x => x.Culture == "default"); // there should be a default one!
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
if (cultureErr != null)
|
||||
{
|
||||
return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery, domainContentId);
|
||||
}
|
||||
}
|
||||
else if (error404Collection.Length == 1)
|
||||
/// <summary>
|
||||
/// Used to determine the node to display when content is not found based on the configured error404 elements in
|
||||
/// umbracoSettings.config
|
||||
/// </summary>
|
||||
internal class NotFoundHandlerHelper
|
||||
{
|
||||
internal static int? GetCurrentNotFoundPageId(
|
||||
ContentErrorPage[] error404Collection,
|
||||
IEntityService entityService,
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
string? errorCulture,
|
||||
int? domainContentId)
|
||||
{
|
||||
if (error404Collection.Length > 1)
|
||||
{
|
||||
// test if a 404 page exists with current culture thread
|
||||
ContentErrorPage? cultureErr =
|
||||
error404Collection.FirstOrDefault(x => x.Culture.InvariantEquals(errorCulture))
|
||||
?? error404Collection.FirstOrDefault(x => x.Culture == "default"); // there should be a default one!
|
||||
|
||||
if (cultureErr != null)
|
||||
{
|
||||
return GetContentIdFromErrorPageConfig(error404Collection.First(), entityService, publishedContentQuery, domainContentId);
|
||||
return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery, domainContentId);
|
||||
}
|
||||
}
|
||||
else if (error404Collection.Length == 1)
|
||||
{
|
||||
return GetContentIdFromErrorPageConfig(error404Collection.First(), entityService, publishedContentQuery, domainContentId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the content id based on the configured ContentErrorPage section.
|
||||
/// </summary>
|
||||
internal static int? GetContentIdFromErrorPageConfig(
|
||||
ContentErrorPage errorPage,
|
||||
IEntityService entityService,
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
int? domainContentId)
|
||||
{
|
||||
if (errorPage.HasContentId)
|
||||
{
|
||||
return errorPage.ContentId;
|
||||
}
|
||||
|
||||
if (errorPage.HasContentKey)
|
||||
{
|
||||
// need to get the Id for the GUID
|
||||
// TODO: When we start storing GUIDs into the IPublishedContent, then we won't have to look this up
|
||||
// but until then we need to look it up in the db. For now we've implemented a cached service for
|
||||
// converting Int -> Guid and vice versa.
|
||||
Attempt<int> found = entityService.GetId(errorPage.ContentKey, UmbracoObjectTypes.Document);
|
||||
if (found.Success)
|
||||
{
|
||||
return found.Result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the content id based on the configured ContentErrorPage section.
|
||||
/// </summary>
|
||||
internal static int? GetContentIdFromErrorPageConfig(
|
||||
ContentErrorPage errorPage,
|
||||
IEntityService entityService,
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
int? domainContentId)
|
||||
if (errorPage.ContentXPath.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
if (errorPage.HasContentId)
|
||||
try
|
||||
{
|
||||
return errorPage.ContentId;
|
||||
}
|
||||
// we have an xpath statement to execute
|
||||
var xpathResult = UmbracoXPathPathSyntaxParser.ParseXPathQuery(
|
||||
errorPage.ContentXPath!,
|
||||
domainContentId,
|
||||
nodeid =>
|
||||
{
|
||||
IEntitySlim? ent = entityService.Get(nodeid);
|
||||
return ent?.Path.Split(',').Reverse();
|
||||
},
|
||||
i => publishedContentQuery.Content(i) != null);
|
||||
|
||||
if (errorPage.HasContentKey)
|
||||
{
|
||||
// need to get the Id for the GUID
|
||||
// TODO: When we start storing GUIDs into the IPublishedContent, then we won't have to look this up
|
||||
// but until then we need to look it up in the db. For now we've implemented a cached service for
|
||||
// converting Int -> Guid and vice versa.
|
||||
Attempt<int> found = entityService.GetId(errorPage.ContentKey, UmbracoObjectTypes.Document);
|
||||
if (found.Success)
|
||||
// now we'll try to execute the expression
|
||||
IPublishedContent? nodeResult = publishedContentQuery.ContentSingleAtXPath(xpathResult);
|
||||
if (nodeResult != null)
|
||||
{
|
||||
return found.Result;
|
||||
return nodeResult.Id;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StaticApplicationLogging.Logger.LogError(ex, "Could not parse xpath expression: {ContentXPath}", errorPage.ContentXPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (errorPage.ContentXPath.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// we have an xpath statement to execute
|
||||
var xpathResult = UmbracoXPathPathSyntaxParser.ParseXPathQuery(
|
||||
xpathExpression: errorPage.ContentXPath!,
|
||||
nodeContextId: domainContentId,
|
||||
getPath: nodeid =>
|
||||
{
|
||||
IEntitySlim? ent = entityService.Get(nodeid);
|
||||
return ent?.Path.Split(',').Reverse();
|
||||
},
|
||||
publishedContentExists: i => publishedContentQuery.Content(i) != null);
|
||||
|
||||
// now we'll try to execute the expression
|
||||
IPublishedContent? nodeResult = publishedContentQuery.ContentSingleAtXPath(xpathResult);
|
||||
if (nodeResult != null)
|
||||
{
|
||||
return nodeResult.Id;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StaticApplicationLogging.Logger.LogError(ex, "Could not parse xpath expression: {ContentXPath}", errorPage.ContentXPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -15,177 +12,186 @@ using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
/// Implements a notification handler for managing redirect URLs tracking.
|
||||
/// <para>when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL</para>
|
||||
/// <para>
|
||||
/// not managing domains because we don't know how to do it - changing domains => must create a higher level
|
||||
/// strategy using rewriting rules probably
|
||||
/// </para>
|
||||
/// <para>recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same</para>
|
||||
public sealed class RedirectTrackingHandler :
|
||||
INotificationHandler<ContentPublishingNotification>,
|
||||
INotificationHandler<ContentPublishedNotification>,
|
||||
INotificationHandler<ContentMovingNotification>,
|
||||
INotificationHandler<ContentMovedNotification>
|
||||
{
|
||||
/// Implements a notification handler for managing redirect URLs tracking.
|
||||
/// <para>when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL</para>
|
||||
/// <para>
|
||||
/// not managing domains because we don't know how to do it - changing domains => must create a higher level
|
||||
/// strategy using rewriting rules probably
|
||||
/// </para>
|
||||
/// <para>recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same</para>
|
||||
public sealed class RedirectTrackingHandler :
|
||||
INotificationHandler<ContentPublishingNotification>,
|
||||
INotificationHandler<ContentPublishedNotification>,
|
||||
INotificationHandler<ContentMovingNotification>,
|
||||
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 IOptionsMonitor<WebRoutingSettings> _webRoutingSettings;
|
||||
|
||||
public RedirectTrackingHandler(
|
||||
ILogger<RedirectTrackingHandler> logger,
|
||||
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
{
|
||||
private readonly ILogger<RedirectTrackingHandler> _logger;
|
||||
private readonly IOptionsMonitor<WebRoutingSettings> _webRoutingSettings;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRedirectUrlService _redirectUrlService;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
_logger = logger;
|
||||
_webRoutingSettings = webRoutingSettings;
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
private const string NotificationStateKey = "Umbraco.Cms.Core.Routing.RedirectTrackingHandler";
|
||||
public void Handle(ContentMovedNotification notification) => CreateRedirectsForOldRoutes(notification);
|
||||
|
||||
public RedirectTrackingHandler(
|
||||
ILogger<RedirectTrackingHandler> logger,
|
||||
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
public void Handle(ContentMovingNotification notification) =>
|
||||
StoreOldRoutes(notification.MoveInfoCollection.Select(m => m.Entity), notification);
|
||||
|
||||
public void Handle(ContentPublishedNotification notification) => CreateRedirectsForOldRoutes(notification);
|
||||
|
||||
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
|
||||
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
|
||||
{
|
||||
_logger = logger;
|
||||
_webRoutingSettings = webRoutingSettings;
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
return;
|
||||
}
|
||||
|
||||
public void Handle(ContentPublishingNotification notification) => StoreOldRoutes(notification.PublishedEntities, notification);
|
||||
|
||||
public void Handle(ContentPublishedNotification notification) => CreateRedirectsForOldRoutes(notification);
|
||||
|
||||
public void Handle(ContentMovingNotification notification) => StoreOldRoutes(notification.MoveInfoCollection.Select(m => m.Entity), notification);
|
||||
|
||||
public void Handle(ContentMovedNotification notification) => CreateRedirectsForOldRoutes(notification);
|
||||
|
||||
private void StoreOldRoutes(IEnumerable<IContent> entities, IStatefulNotification notification)
|
||||
OldRoutesDictionary oldRoutes = GetOldRoutes(notification);
|
||||
foreach (IContent entity in entities)
|
||||
{
|
||||
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
|
||||
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
|
||||
return;
|
||||
StoreOldRoute(entity, oldRoutes);
|
||||
}
|
||||
}
|
||||
|
||||
var oldRoutes = GetOldRoutes(notification);
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
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
|
||||
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private void CreateRedirectsForOldRoutes(IStatefulNotification notification)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
var oldRoutes = GetOldRoutes(notification);
|
||||
CreateRedirects(oldRoutes);
|
||||
private OldRoutesDictionary GetOldRoutes(IStatefulNotification notification)
|
||||
{
|
||||
if (notification.State.ContainsKey(NotificationStateKey) == false)
|
||||
{
|
||||
notification.State[NotificationStateKey] = new OldRoutesDictionary();
|
||||
}
|
||||
|
||||
private OldRoutesDictionary GetOldRoutes(IStatefulNotification notification)
|
||||
{
|
||||
if (notification.State.ContainsKey(NotificationStateKey) == false)
|
||||
{
|
||||
notification.State[NotificationStateKey] = new OldRoutesDictionary();
|
||||
}
|
||||
return (OldRoutesDictionary?)notification.State[NotificationStateKey] ?? new OldRoutesDictionary();
|
||||
}
|
||||
|
||||
return (OldRoutesDictionary?)notification.State[NotificationStateKey] ?? new OldRoutesDictionary();
|
||||
private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes)
|
||||
{
|
||||
if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes)
|
||||
IPublishedContentCache? contentCache = publishedSnapshot?.Content;
|
||||
IPublishedContent? entityContent = contentCache?.GetById(entity.Id);
|
||||
if (entityContent is null)
|
||||
{
|
||||
if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out var 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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] = new ContentKeyAndOldRoute(publishedContent.Key, route!);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private void CreateRedirects(OldRoutesDictionary oldRoutes)
|
||||
// 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 (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// if this entity defines specific cultures, use those instead of the default ones
|
||||
IEnumerable<string> cultures =
|
||||
publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures;
|
||||
|
||||
var contentCache = publishedSnapshot?.Content;
|
||||
|
||||
if (contentCache == null)
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
_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)
|
||||
var route = contentCache?.GetRouteById(publishedContent.Id, culture);
|
||||
if (IsNotRoute(route))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture);
|
||||
oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] =
|
||||
new ContentKeyAndOldRoute(publishedContent.Key, route!);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNotRoute(string? route)
|
||||
{
|
||||
// null if content not found
|
||||
// err/- if collision or anomaly or ...
|
||||
return route == null || route.StartsWith("err/");
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user