using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Routing; /// /// Provides an implementation of that handles page aliases. /// /// /// /// Handles /just/about/anything where /just/about/anything is contained in the /// umbracoUrlAlias property of a document. /// /// The alias is the full path to the document. There can be more than one alias, separated by commas. /// public class ContentFinderByUrlAlias : IContentFinder { private readonly ILogger _logger; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IVariationContextAccessor _variationContextAccessor; /// /// Initializes a new instance of the class. /// public ContentFinderByUrlAlias( ILogger logger, IPublishedValueFallback publishedValueFallback, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) { _publishedValueFallback = publishedValueFallback; _variationContextAccessor = variationContextAccessor; _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; } /// /// Tries to find and assign an Umbraco document to a PublishedRequest. /// /// The PublishedRequest. /// A value indicating whether an Umbraco document was found and assigned. public Task TryFindContent(IPublishedRequestBuilder frequest) { if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { return Task.FromResult(false); } IPublishedContent? node = null; // no alias if "/" if (frequest.Uri.AbsolutePath != "/") { node = FindContentByAlias( umbracoContext.Content, frequest.Domain != null ? frequest.Domain.ContentId : 0, frequest.Culture, frequest.AbsolutePathDecoded); if (node != null) { frequest.SetPublishedContent(node); if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( "Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); } } } return Task.FromResult(node != null); } private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) { if (alias == null) { throw new ArgumentNullException(nameof(alias)); } // the alias may be "foo/bar" or "/foo/bar" // there may be spaces as in "/foo/bar, /foo/nil" // these should probably be taken care of earlier on // TODO: can we normalize the values so that they contain no whitespaces, and no leading slashes? // and then the comparisons in IsMatch can be way faster - and allocate way less strings const string propertyAlias = Constants.Conventions.Content.UrlAlias; var test1 = alias.TrimStart(Constants.CharArrays.ForwardSlash) + ","; var test2 = ",/" + test1; // test2 is ",/alias," test1 = "," + test1; // test1 is ",alias," bool IsMatch(IPublishedContent c, string a1, string a2) { // this basically implements the original XPath query ;-( // // "//* [@isDoc and (" + // "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + // " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + // ")]" if (!c.HasProperty(propertyAlias)) { return false; } IPublishedProperty? p = c.GetProperty(propertyAlias); var varies = p?.PropertyType?.VariesByCulture(); string? v; if (varies ?? false) { if (!c.HasCulture(culture)) { return false; } v = c.Value(_publishedValueFallback, propertyAlias, culture); } else { v = c.Value(_publishedValueFallback, propertyAlias); } if (string.IsNullOrWhiteSpace(v)) { return false; } v = "," + v.Replace(" ", string.Empty) + ","; return v.InvariantContains(a1) || v.InvariantContains(a2); } // TODO: even with Linq, what happens below has to be horribly slow // but the only solution is to entirely refactor URL providers to stop being dynamic if (rootNodeId > 0) { IPublishedContent? rootNode = cache?.GetById(rootNodeId); return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); } if (cache is not null) { foreach (IPublishedContent rootContent in cache.GetAtRoot()) { IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor) .FirstOrDefault(x => IsMatch(x, test1, test2)); if (c != null) { return c; } } } return null; } }