Files
Umbraco-CMS/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
2020-02-24 08:21:53 +01:00

215 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web.Routing
{
internal static class UrlProviderExtensions
{
/// <summary>
/// Gets the Urls of the content item.
/// </summary>
/// <remarks>
/// <para>Use when displaying Urls. If errors occur when generating the Urls, they will show in the list.</para>
/// <para>Contains all the Urls that we can figure out (based upon domains, etc).</para>
/// </remarks>
public static IEnumerable<UrlInfo> GetContentUrls(this IContent content,
IPublishedRouter publishedRouter,
IUmbracoContext umbracoContext,
ILocalizationService localizationService,
ILocalizedTextService textService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILogger logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (publishedRouter == null) throw new ArgumentNullException(nameof(publishedRouter));
if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
if (localizationService == null) throw new ArgumentNullException(nameof(localizationService));
if (textService == null) throw new ArgumentNullException(nameof(textService));
if (contentService == null) throw new ArgumentNullException(nameof(contentService));
if (logger == null) throw new ArgumentNullException(nameof(logger));
if (publishedUrlProvider == null) throw new ArgumentNullException(nameof(publishedUrlProvider));
if (uriUtility == null) throw new ArgumentNullException(nameof(uriUtility));
if (variationContextAccessor == null) throw new ArgumentNullException(nameof(variationContextAccessor));
if (content.Published == false)
{
yield return UrlInfo.Message(textService.Localize("content/itemNotPublished"));
yield break;
}
// build a list of urls, for the back-office
// which will contain
// - the 'main' urls, which is what .Url would return, for each culture
// - the 'other' urls we know (based upon domains, etc)
//
// need to work through each installed culture:
// on invariant nodes, each culture returns the same url segment but,
// we don't know if the branch to this content is invariant, so we need to ask
// for URLs for all cultures.
// and, not only for those assigned to domains in the branch, because we want
// to show what GetUrl() would return, for every culture.
var urls = new HashSet<UrlInfo>();
var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList();
//get all URLs for all cultures
//in a HashSet, so de-duplicates too
foreach (var cultureUrl in GetContentUrlsByCulture(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider))
{
urls.Add(cultureUrl);
}
//return the real urls first, then the messages
foreach (var urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key))
{
//in some cases there will be the same URL for multiple cultures:
// * The entire branch is invariant
// * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed
foreach (var dUrl in urlGroup.DistinctBy(x => x.Text.ToUpperInvariant()).OrderBy(x => x.Text).ThenBy(x => x.Culture))
yield return dUrl;
}
// get the 'other' urls - ie not what you'd get with GetUrl() but urls that would route to the document, nevertheless.
// for these 'other' urls, we don't check whether they are routable, collide, anything - we just report them.
foreach (var otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
if (urls.Add(otherUrl)) //avoid duplicates
yield return otherUrl;
}
/// <summary>
/// Tries to return a <see cref="UrlInfo"/> for each culture for the content while detecting collisions/errors
/// </summary>
/// <param name="content"></param>
/// <param name="cultures"></param>
/// <param name="publishedRouter"></param>
/// <param name="umbracoContext"></param>
/// <param name="contentService"></param>
/// <param name="textService"></param>
/// <param name="logger"></param>
/// <returns></returns>
private static IEnumerable<UrlInfo> GetContentUrlsByCulture(IContent content,
IEnumerable<string> cultures,
IPublishedRouter publishedRouter,
IUmbracoContext umbracoContext,
IContentService contentService,
ILocalizedTextService textService,
IVariationContextAccessor variationContextAccessor,
ILogger logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
foreach (var culture in cultures)
{
// if content is variant, and culture is not published, skip
if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture))
continue;
// if it's variant and culture is published, or if it's invariant, proceed
string url;
try
{
url = publishedUrlProvider.GetUrl(content.Id, culture: culture);
}
catch (Exception ex)
{
logger.Error<UrlProvider>(ex, "GetUrl exception.");
url = "#ex";
}
switch (url)
{
// deal with 'could not get the url'
case "#":
yield return HandleCouldNotGetUrl(content, culture, contentService, textService);
break;
// deal with exceptions
case "#ex":
yield return UrlInfo.Message(textService.Localize("content/getUrlException"), culture);
break;
// got a url, deal with collisions, add url
default:
if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, out var urlInfo)) // detect collisions, etc
yield return urlInfo;
else
yield return UrlInfo.Url(url, culture);
break;
}
}
}
private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService)
{
// document has a published version yet its url is "#" => a parent must be
// unpublished, walk up the tree until we find it, and report.
var parent = content;
do
{
parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null;
}
while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
if (parent == null) // oops, internal error
return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture);
else if (!parent.Published) // totally not published
return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] {parent.Name}), culture);
else // culture not published
return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture);
}
private static bool DetectCollision(IContent content, string url, string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility, out UrlInfo urlInfo)
{
// test for collisions on the 'main' url
var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute);
if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl);
uri = uriUtility.UriToUmbraco(uri);
var pcr = publishedRouter.CreateRequest(umbracoContext, uri);
publishedRouter.TryRouteRequest(pcr);
urlInfo = null;
if (pcr.HasPublishedContent == false)
{
urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture);
return true;
}
if (pcr.IgnorePublishedContentCollisions)
return false;
if (pcr.PublishedContent.Id != content.Id)
{
var o = pcr.PublishedContent;
var l = new List<string>();
while (o != null)
{
l.Add(o.Name(variationContextAccessor));
o = o.Parent;
}
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture);
return true;
}
// no collision
return false;
}
}
}