Merge branch 'temp8-3658-contextual-info-app' into temp8

This commit is contained in:
Stephan
2018-12-20 14:32:03 +01:00
17 changed files with 410 additions and 182 deletions
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Web.Routing;
namespace Umbraco.Tests.Routing
{
[TestFixture]
public class GetContentUrlsTests : UrlRoutingTestBase
{
private IUmbracoSettingsSection _umbracoSettings;
public override void SetUp()
{
base.SetUp();
//generate new mock settings and assign so we can configure in individual tests
_umbracoSettings = SettingsForTests.GenerateMockUmbracoSettings();
SettingsForTests.ConfigureSettings(_umbracoSettings);
}
private ILocalizedTextService GetTextService()
{
var textService = Mock.Of<ILocalizedTextService>(
x => x.Localize("content/itemNotPublished",
It.IsAny<CultureInfo>(),
It.IsAny<IDictionary<string, string>>()) == "content/itemNotPublished");
return textService;
}
private ILocalizationService GetLangService(params string[] isoCodes)
{
var allLangs = isoCodes
.Select(CultureInfo.GetCultureInfo)
.Select(culture => new Language(culture.Name)
{
CultureName = culture.DisplayName,
IsDefault = true,
IsMandatory = true
}).ToArray();
var langService = Mock.Of<ILocalizationService>(x => x.GetAllLanguages() == allLangs);
return langService;
}
[Test]
public void Content_Not_Published()
{
var contentType = MockedContentTypes.CreateBasicContentType();
var content = MockedContent.CreateBasicContent(contentType);
content.Id = 1046; //fixme: we are using this ID only because it's built into the test XML published cache
content.Path = "-1,1046";
var umbContext = GetUmbracoContext("http://localhost:8000");
var publishedRouter = CreatePublishedRouter(Container,
contentFinders: new ContentFinderCollection(new[] { new ContentFinderByUrl(Logger) }));
var urls = content.GetContentUrls(publishedRouter,
umbContext,
GetLangService("en-US", "fr-FR"), GetTextService(), ServiceContext.ContentService,
Logger).ToList();
Assert.AreEqual(1, urls.Count);
Assert.AreEqual("content/itemNotPublished", urls[0].Text);
Assert.IsFalse(urls[0].IsUrl);
}
[Test]
public void Invariant_Root_Content_Published_No_Domains()
{
var contentType = MockedContentTypes.CreateBasicContentType();
var content = MockedContent.CreateBasicContent(contentType);
content.Id = 1046; //fixme: we are using this ID only because it's built into the test XML published cache
content.Path = "-1,1046";
content.Published = true;
var umbContext = GetUmbracoContext("http://localhost:8000",
urlProviders: new []{ new DefaultUrlProvider(_umbracoSettings.RequestHandler, Logger, TestObjects.GetGlobalSettings(), new SiteDomainHelper()) });
var publishedRouter = CreatePublishedRouter(Container,
contentFinders:new ContentFinderCollection(new[]{new ContentFinderByUrl(Logger) }));
var urls = content.GetContentUrls(publishedRouter,
umbContext,
GetLangService("en-US", "fr-FR"), GetTextService(), ServiceContext.ContentService,
Logger).ToList();
Assert.AreEqual(1, urls.Count);
Assert.AreEqual("/home/", urls[0].Text);
Assert.AreEqual("en-US", urls[0].Culture);
Assert.IsTrue(urls[0].IsUrl);
}
[Test]
public void Invariant_Child_Content_Published_No_Domains()
{
var contentType = MockedContentTypes.CreateBasicContentType();
var parent = MockedContent.CreateBasicContent(contentType);
parent.Id = 1046; //fixme: we are using this ID only because it's built into the test XML published cache
parent.Name = "home";
parent.Path = "-1,1046";
parent.Published = true;
var child = MockedContent.CreateBasicContent(contentType);
child.Name = "sub1";
child.Id = 1173; //fixme: we are using this ID only because it's built into the test XML published cache
child.Path = "-1,1046,1173";
child.Published = true;
var umbContext = GetUmbracoContext("http://localhost:8000",
urlProviders: new[] { new DefaultUrlProvider(_umbracoSettings.RequestHandler, Logger, TestObjects.GetGlobalSettings(), new SiteDomainHelper()) });
var publishedRouter = CreatePublishedRouter(Container,
contentFinders: new ContentFinderCollection(new[] { new ContentFinderByUrl(Logger) }));
var urls = child.GetContentUrls(publishedRouter,
umbContext,
GetLangService("en-US", "fr-FR"), GetTextService(), ServiceContext.ContentService,
Logger).ToList();
Assert.AreEqual(1, urls.Count);
Assert.AreEqual("/home/sub1/", urls[0].Text);
Assert.AreEqual("en-US", urls[0].Culture);
Assert.IsTrue(urls[0].IsUrl);
}
//TODO: We need a lot of tests here, the above was just to get started with being able to unit test this method
// * variant URLs without domains assigned, what happens?
// * variant URLs with domains assigned, but also having more languages installed than there are domains/cultures assigned
// * variant URLs with an ancestor culture unpublished
// * invariant URLs with ancestors as variants
// * ... probably a lot more
}
}
@@ -418,8 +418,8 @@ namespace Umbraco.Tests.Routing
foreach (var x in result) Console.WriteLine(x);
Assert.AreEqual(2, result.Length);
Assert.IsTrue(result.Contains("http://domain1a.com/en/1001-1-1/"));
Assert.IsTrue(result.Contains("http://domain1b.com/en/1001-1-1/"));
Assert.AreEqual(result[0].Text, "http://domain1b.com/en/1001-1-1/");
Assert.AreEqual(result[1].Text, "http://domain1a.com/en/1001-1-1/");
}
}
}
@@ -159,7 +159,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
var urlHelper = new Mock<IUrlProvider>();
urlHelper.Setup(provider => provider.GetUrl(It.IsAny<UmbracoContext>(), It.IsAny<IPublishedContent>(), It.IsAny<UrlProviderMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
.Returns("/hello/world/1234");
.Returns(UrlInfo.Url("/hello/world/1234"));
var membershipHelper = new MembershipHelper(umbCtx, Mock.Of<MembershipProvider>(), Mock.Of<RoleProvider>());
@@ -75,7 +75,7 @@ namespace Umbraco.Tests.Testing.TestingTests
var urlProviderMock = new Mock<IUrlProvider>();
urlProviderMock.Setup(provider => provider.GetUrl(It.IsAny<UmbracoContext>(), It.IsAny<IPublishedContent>(), It.IsAny<UrlProviderMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
.Returns("/hello/world/1234");
.Returns(UrlInfo.Url("/hello/world/1234"));
var urlProvider = urlProviderMock.Object;
var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor);
+1
View File
@@ -132,6 +132,7 @@
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />
<Compile Include="Services\ContentServiceTagsTests.cs" />
<Compile Include="Services\ContentTypeServiceVariantsTests.cs" />
@@ -79,7 +79,7 @@ namespace Umbraco.Tests.Web
var testUrlProvider = new Mock<IUrlProvider>();
testUrlProvider
.Setup(x => x.GetUrl(It.IsAny<UmbracoContext>(), It.IsAny<IPublishedContent>(), It.IsAny<UrlProviderMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
.Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => "/my-test-url");
.Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => UrlInfo.Url("/my-test-url"));
var globalSettings = SettingsForTests.GenerateMockGlobalSettings();
@@ -10,6 +10,8 @@
var auditTrailLoaded = false;
var labels = {};
scope.publishStatus = [];
scope.currentVariant = null;
scope.currentUrls = [];
scope.disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates;
scope.allowChangeDocumentType = false;
@@ -17,6 +19,18 @@
function onInit() {
// set currentVariant
scope.currentVariant = _.find(scope.node.variants, (v) => v.active);
// find the urls for the currently selected language
if(scope.node.variants.length > 1) {
// nodes with variants
scope.currentUrls = _.filter(scope.node.urls, (url) => scope.currentVariant.language.culture === url.culture);
} else {
// invariant nodes
scope.currentUrls = scope.node.urls;
}
// if there are any infinite editors open we are in infinite editing
scope.isInfiniteMode = editorService.getNumberOfEditors() > 0 ? true : false;
@@ -34,7 +48,10 @@
"content_publishedPendingChanges",
"content_notCreated",
"prompt_unsavedChanges",
"prompt_doctypeChangeWarning"
"prompt_doctypeChangeWarning",
"general_history",
"auditTrails_historyIncludingVariants",
"content_itemNotPublished"
];
localizationService.localizeMany(keys)
@@ -46,8 +63,22 @@
labels.notCreated = data[4];
labels.unsavedChanges = data[5];
labels.doctypeChangeWarning = data[6];
labels.notPublished = data[9];
scope.historyLabel = scope.node.variants && scope.node.variants.length === 1 ? data[7] : data[8];
setNodePublishStatus();
setNodePublishStatus(scope.node);
if (scope.currentUrls.length === 0) {
if (scope.node.id > 0) {
//it's created but not published
scope.currentUrls.push({ text: labels.notPublished, isUrl: false });
}
else {
//it's new
scope.currentUrls.push({ text: labels.notCreated, isUrl: false })
}
}
});
@@ -55,6 +86,9 @@
"id": scope.node.id
};
// make sure dates are formatted to the user's locale
formatDatesToLocal();
// get available templates
scope.availableTemplates = scope.node.allowedTemplates;
@@ -218,12 +252,13 @@
function setAuditTrailLogTypeColor(auditTrail) {
angular.forEach(auditTrail, function (item) {
switch (item.logType) {
case "Publish":
case "PublishVariant":
item.logTypeColor = "success";
break;
case "Unpublish":
case "UnpublishVariant":
case "Delete":
item.logTypeColor = "danger";
break;
@@ -233,51 +268,40 @@
});
}
function setNodePublishStatus(node) {
function setNodePublishStatus() {
scope.publishStatus = [];
scope.status = {};
// deleted node
if (node.trashed === true) {
scope.publishStatus.push({
label: labels.deleted,
color: "danger"
});
if (scope.node.trashed === true) {
scope.status.color = "danger";
return;
}
if (node.variants) {
for (var i = 0; i < node.variants.length; i++) {
var variant = node.variants[i];
var status = {
culture: variant.language ? variant.language.culture : null
};
if (variant.state === "NotCreated") {
status.label = labels.notCreated;
status.color = "gray";
}
else if (variant.state === "Draft") {
// draft node
status.label = labels.unpublished;
status.color = "gray";
}
else if (variant.state === "Published") {
// published node
status.label = labels.published;
status.color = "success";
}
else if (variant.state === "PublishedPendingChanges") {
// published node with pending changes
status.label = labels.publishedPendingChanges;
status.color = "success";
}
scope.publishStatus.push(status);
}
// variant status
if (scope.currentVariant.state === "NotCreated") {
// not created
scope.status.color = "gray";
}
else if (scope.currentVariant.state === "Draft") {
// draft node
scope.status.color = "gray";
}
else if (scope.currentVariant.state === "Published") {
// published node
scope.status.color = "success";
}
else if (scope.currentVariant.state === "PublishedPendingChanges") {
// published node with pending changes
scope.status.color = "success";
}
}
function formatDatesToLocal() {
// get current backoffice user and format dates
userService.getCurrentUser().then(function (currentUser) {
scope.currentVariant.createDateFormatted = dateHelper.getLocalDate(scope.currentVariant.createDate, currentUser.locale, 'LLL');
});
}
// load audit trail and redirects when on the info tab
@@ -303,7 +327,7 @@
auditTrailLoaded = false;
loadAuditTrail();
loadRedirectUrls();
setNodePublishStatus(scope.node);
setNodePublishStatus();
}
});
@@ -2,21 +2,25 @@
<div class="umb-package-details__main-content">
<umb-box ng-if="node.urls" data-element="node-info-urls">
<umb-box ng-if="currentUrls" data-element="node-info-urls">
<umb-box-header title-key="general_links"></umb-box-header>
<umb-box-content class="block-form">
<ul class="nav nav-stacked" style="margin-bottom: 0;">
<li ng-repeat="url in node.urls">
<li ng-repeat="url in currentUrls">
<span ng-if="url.isUrl">
<span ng-if="node.variants.length === 1 && url.culture" style="font-size: 13px; color: #cccccc; width: 50px;display: inline-block">{{url.culture}}</span>
<a href="{{url.text}}" target="_blank">
<i class="icon icon-window-popin"></i>
<span>{{url.text}}</span>
</a>
<span ng-if="url.culture" style="font-size: 13px; color: #cccccc; margin-left: 20px;">{{url.culture}}</span>
</span>
<div ng-if="!url.isUrl" style="margin-top: 4px;">
<span>{{url.text}}</span>
<span ng-if="url.culture" style="font-size: 13px; color: #cccccc; margin-left: 20px;">{{url.culture}}</span>
<span ng-if="node.variants.length === 1 && url.culture" style="font-size: 13px; color: #cccccc; width: 50px;display: inline-block">{{url.culture}}</span>
<em>{{url.text}}</em>
</div>
</li>
</ul>
@@ -43,7 +47,8 @@
<umb-box data-element="node-info-history">
<umb-box-header
title-key="general_history">
title="{{historyLabel}}">
<umb-button
type="button"
button-style="outline"
@@ -62,8 +67,9 @@
<umb-load-indicator ng-show="loadingAuditTrail"></umb-load-indicator>
<div ng-show="auditTrail.length === 0" style="padding: 10px;">
<umb-empty-state position="center"
size="small">
<umb-empty-state
position="center"
size="small">
<localize key="content_noChanges"></localize>
</umb-empty-state>
</div>
@@ -96,7 +102,6 @@
class="history-item__badge"
size="xs"
color="{{item.logTypeColor}}">
<localize key="auditTrails_small{{ item.logType }}">{{ item.logType }}</localize>
</umb-badge>
<span>
@@ -130,16 +135,13 @@
<umb-box-content class="block-form">
<umb-control-group data-element="node-info-status" label="@general_status">
<div ng-repeat="status in publishStatus" style="margin-bottom: 5px;">
<span ng-show="status.culture"><em>{{status.culture}}: </em></span>
<umb-badge size="xs" color="{{status.color}}">
{{status.label}}
</umb-badge>
</div>
<umb-badge size="xs" color="{{status.color}}">
<umb-variant-state variant="currentVariant"></umb-variant-state>
</umb-badge>
</umb-control-group>
<umb-control-group ng-show="node.id !== 0" data-element="node-info-create-date" label="@template_createdDate">
{{node.createDateFormatted}} <localize key="general_by">by</localize> {{ node.owner.name }}
{{currentVariant.createDateFormatted}}
</umb-control-group>
<umb-control-group data-element="node-info-document-type" label="@content_documentType">
@@ -147,8 +147,9 @@
<key alias="atViewingFor">Viewing for</key>
<key alias="delete">Content deleted</key>
<key alias="unpublish">Content unpublished</key>
<key alias="publish">Content saved and Published</key>
<key alias="publishvariant">Content saved and published for languages: %0% </key>
<key alias="unpublishvariant">Content unpublished for languages: %0% </key>
<key alias="publish">Content published</key>
<key alias="publishvariant">Content published for languages: %0% </key>
<key alias="save">Content saved</key>
<key alias="savevariant">Content saved for languages: %0%</key>
<key alias="move">Content moved</key>
@@ -165,10 +166,12 @@
<key alias="smallSaveVariant">Save</key>
<key alias="smallDelete">Delete</key>
<key alias="smallUnpublish">Unpublish</key>
<key alias="smallUnpublishVariant">Unpublish</key>
<key alias="smallRollBack">Rollback</key>
<key alias="smallSendToPublish">Send To Publish</key>
<key alias="smallSendToPublishVariant">Send To Publish</key>
<key alias="smallSort">Sort</key>
<key alias="historyIncludingVariants">History (all variants)</key>
</area>
<area alias="changeDocType">
<key alias="changeDocTypeInstruction">To change the document type for the selected content, first select from the list of valid types for this location.</key>
+14 -15
View File
@@ -31,7 +31,7 @@ namespace Umbraco.Web.Routing
#region GetUrl
/// <inheritdoc />
public string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
{
return null; // we have nothing to say
}
@@ -51,13 +51,14 @@ namespace Umbraco.Web.Routing
/// <para>Other urls are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// </remarks>
public IEnumerable<string> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
public IEnumerable<UrlInfo> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
{
var node = umbracoContext.ContentCache.GetById(id);
if (node == null) return Enumerable.Empty<string>();
if (node == null)
yield break;
if (!node.HasProperty(Constants.Conventions.Content.UrlAlias))
return Enumerable.Empty<string>();
yield break;
var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper);
@@ -82,20 +83,19 @@ namespace Umbraco.Web.Routing
// the content finder may work, depending on the 'current' culture,
// but there's no way we can return something meaningful here
if (varies)
return Enumerable.Empty<string>();
yield break;
var umbracoUrlName = node.Value<string>(Constants.Conventions.Content.UrlAlias);
if (string.IsNullOrWhiteSpace(umbracoUrlName))
return Enumerable.Empty<string>();
yield break;
var path = "/" + umbracoUrlName;
var uri = new Uri(path, UriKind.Relative);
return new[] { UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString() };
yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString());
}
else
{
// some domains: one url per domain, which is "<domain>/<alias>"
var result = new List<string>();
foreach(var domainUri in domainUris)
{
// if the property is invariant, get the invariant value, url is "<domain>/<invariant-alias>"
@@ -108,14 +108,13 @@ namespace Umbraco.Web.Routing
? node.Value<string>(Constants.Conventions.Content.UrlAlias, culture: domainUri.Culture.Name)
: node.Value<string>(Constants.Conventions.Content.UrlAlias);
if (!string.IsNullOrWhiteSpace(umbracoUrlName))
{
var path = "/" + umbracoUrlName;
var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path));
result.Add(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString());
}
if (string.IsNullOrWhiteSpace(umbracoUrlName))
continue;
var path = "/" + umbracoUrlName;
var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path));
yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString(), domainUri.Culture.Name);
}
return result;
}
}
@@ -15,7 +15,7 @@ namespace Umbraco.Web.Routing
/// <summary>
/// This will return the URL that is returned by the assigned custom <see cref="IPublishedContent"/> if this is a custom route
/// </summary>
public string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
{
if (umbracoContext?.PublishedRequest?.PublishedContent == null) return null;
if (umbracoContext.HttpContext?.Request?.RequestContext?.RouteData?.DataTokens == null) return null;
@@ -26,13 +26,15 @@ namespace Umbraco.Web.Routing
//NOTE: This looks like it might cause an infinite loop because PublishedContentBase.Url calls into UmbracoContext.Current.UrlProvider.GetUrl which calls back into the IUrlProvider pipeline
// but the specific purpose of this is that a developer is using their own IPublishedContent that returns a specific Url and doesn't go back into the UrlProvider pipeline.
//TODO: We could put a try/catch here just in case, else we could do some reflection checking to see if the implementation is PublishedContentBase and the Url property is not overridden.
return content.Id == umbracoContext.PublishedRequest.PublishedContent.Id
? umbracoContext.PublishedRequest.PublishedContent.GetUrl(culture)
: null;
return UrlInfo.Url(
content.Id == umbracoContext.PublishedRequest.PublishedContent.Id
? umbracoContext.PublishedRequest.PublishedContent.GetUrl(culture)
: null,
culture);
}
/// <summary>
/// This always returns null because this url provider is used purely to deal with Umbraco custom routes with
/// This always returns an empty result because this url provider is used purely to deal with Umbraco custom routes with
/// UmbracoVirtualNodeRouteHandler, we really only care about the normal URL so that RedirectToCurrentUmbracoPage() works
/// with SurfaceControllers
/// </summary>
@@ -40,9 +42,9 @@ namespace Umbraco.Web.Routing
/// <param name="id"></param>
/// <param name="current"></param>
/// <returns></returns>
public IEnumerable<string> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
public IEnumerable<UrlInfo> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
{
return null;
yield break;
}
}
}
+15 -12
View File
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Routing
#region GetUrl
/// <inheritdoc />
public virtual string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current)
{
if (!current.IsAbsoluteUri) throw new ArgumentException("Current url must be absolute.", nameof(current));
@@ -39,7 +39,7 @@ namespace Umbraco.Web.Routing
return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture);
}
internal string GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture)
internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture)
{
if (string.IsNullOrWhiteSpace(route))
{
@@ -58,7 +58,9 @@ namespace Umbraco.Web.Routing
: domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current, culture);
// assemble the url from domainUri (maybe null) and path
return AssembleUrl(domainUri, path, current, mode).ToString();
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
}
#endregion
@@ -76,10 +78,11 @@ namespace Umbraco.Web.Routing
/// <para>Other urls are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// </remarks>
public virtual IEnumerable<string> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
public virtual IEnumerable<UrlInfo> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
{
var node = umbracoContext.ContentCache.GetById(id);
if (node == null) return Enumerable.Empty<string>();
if (node == null)
yield break;
var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper);
@@ -94,13 +97,14 @@ namespace Umbraco.Web.Routing
// no domains = exit
if (domainUris ==null)
return Enumerable.Empty<string>();
yield break;
var result = new List<string>();
foreach (var d in domainUris)
{
var culture = d?.Culture?.Name;
//although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok
var route = umbracoContext.ContentCache.GetRouteById(id, d?.Culture?.Name);
var route = umbracoContext.ContentCache.GetRouteById(id, culture);
if (route == null) continue;
//need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned)
@@ -109,9 +113,8 @@ namespace Umbraco.Web.Routing
var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path));
uri = UriUtility.UriFromUmbraco(uri, _globalSettings, _requestSettings);
result.Add(uri.ToString());
yield return UrlInfo.Url(uri.ToString(), culture);
}
return result;
}
#endregion
@@ -146,7 +149,7 @@ namespace Umbraco.Web.Routing
uri = new Uri(path, UriKind.Relative);
break;
default:
throw new ArgumentOutOfRangeException("mode");
throw new ArgumentOutOfRangeException(nameof(mode));
}
}
else // a domain was found
@@ -169,7 +172,7 @@ namespace Umbraco.Web.Routing
uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative);
break;
default:
throw new ArgumentOutOfRangeException("mode");
throw new ArgumentOutOfRangeException(nameof(mode));
}
}
+9 -15
View File
@@ -30,10 +30,10 @@ namespace Umbraco.Web.Routing
/// <param name="culture">The culture, or null.</param>
/// <returns>The domain and its uri, if any, that best matches the specified uri and culture, else null.</returns>
/// <remarks>
/// <para>f at least a domain is set on the node then the method returns the domain that
/// <para>If at least a domain is set on the node then the method returns the domain that
/// best matches the specified uri and culture, else it returns null.</para>
/// <para>If culture is null, uses the default culture for the installation instead.</para>
/// fixme not exactly - if culture is !null, we MUST have a value for THAT culture, else we use default as hint
/// <para>If culture is null, uses the default culture for the installation instead. Otherwise,
/// will try with the specified culture, else return null.</para>
/// </remarks>
internal DomainAndUri DomainForNode(int nodeId, Uri current, string culture = null)
{
@@ -49,13 +49,9 @@ namespace Umbraco.Web.Routing
return null;
// else filter
var domainAndUri = SelectDomain(domains, current, culture, _domainCache.DefaultCulture,
// it could be that none apply (due to culture)
return SelectDomain(domains, current, culture, _domainCache.DefaultCulture,
(cdomainAndUris, ccurrent, cculture, cdefaultCulture) => _siteDomainHelper.MapDomain(cdomainAndUris, ccurrent, cculture, cdefaultCulture));
if (domainAndUri == null)
throw new Exception("DomainForUri returned null.");
return domainAndUri;
}
/// <summary>
@@ -202,13 +198,12 @@ namespace Umbraco.Web.Routing
private static IReadOnlyCollection<DomainAndUri> SelectByCulture(IReadOnlyCollection<DomainAndUri> domainsAndUris, string culture, string defaultCulture)
{
// we try our best to match cultures, but may end with a bogus domain
if (culture != null) // try the supplied culture
{
var cultureDomains = domainsAndUris.Where(x => x.Culture.Name.InvariantEquals(culture)).ToList();
if (cultureDomains.Count > 0) return cultureDomains;
// if a culture is supplied, we *want* a url for that culture, else fail
throw new InvalidOperationException($"Could not find a domain for culture \"{culture}\".");
}
if (defaultCulture != null) // try the defaultCulture culture
@@ -224,13 +219,12 @@ namespace Umbraco.Web.Routing
{
DomainAndUri domainAndUri;
// we try our best to match cultures, but may end with a bogus domain
if (culture != null) // try the supplied culture
{
domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.Name.InvariantEquals(culture));
if (domainAndUri != null) return domainAndUri;
// if a culture is supplied, we *want* a url for that culture, else fail
throw new InvalidOperationException($"Could not find a domain for culture \"{culture}\".");
}
if (defaultCulture != null) // try the defaultCulture culture
+2 -2
View File
@@ -24,7 +24,7 @@ namespace Umbraco.Web.Routing
/// when no culture is specified, the current culture.</para>
/// <para>If the provider is unable to provide a url, it should return <c>null</c>.</para>
/// </remarks>
string GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current);
UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current);
/// <summary>
/// Gets the other urls of a published content.
@@ -37,6 +37,6 @@ namespace Umbraco.Web.Routing
/// <para>Other urls are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// </remarks>
IEnumerable<string> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current);
IEnumerable<UrlInfo> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current);
}
}
+55 -8
View File
@@ -1,4 +1,5 @@
using System.Runtime.Serialization;
using System;
using System.Runtime.Serialization;
namespace Umbraco.Web.Routing
{
@@ -6,13 +7,25 @@ namespace Umbraco.Web.Routing
/// Represents infos for a url.
/// </summary>
[DataContract(Name = "urlInfo", Namespace = "")]
public class UrlInfo
public class UrlInfo : IEquatable<UrlInfo>
{
/// <summary>
/// Creates a <see cref="UrlInfo"/> instance representing a true url.
/// </summary>
public static UrlInfo Url(string text, string culture = null) => new UrlInfo(text, true, culture);
/// <summary>
/// Creates a <see cref="UrlInfo"/> instance representing a message.
/// </summary>
public static UrlInfo Message(string text, string culture = null) => new UrlInfo(text, false, culture);
/// <summary>
/// Initializes a new instance of the <see cref="UrlInfo"/> class.
/// </summary>
public UrlInfo(string text, bool isUrl, string culture)
{
if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(text));
IsUrl = isUrl;
Text = text;
Culture = culture;
@@ -38,13 +51,47 @@ namespace Umbraco.Web.Routing
public string Text { get; }
/// <summary>
/// Creates a <see cref="UrlInfo"/> instance representing a true url.
/// Checks equality
/// </summary>
public static UrlInfo Url(string text, string culture = null) => new UrlInfo(text, true, culture);
/// <param name="other"></param>
/// <returns></returns>
/// <remarks>
/// Compare both culture and Text as invariant strings since URLs are not case sensitive, nor are culture names within Umbraco
/// </remarks>
public bool Equals(UrlInfo other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(Culture, other.Culture, StringComparison.InvariantCultureIgnoreCase) && IsUrl == other.IsUrl && string.Equals(Text, other.Text, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Creates a <see cref="UrlInfo"/> instance representing a message.
/// </summary>
public static UrlInfo Message(string text, string culture = null) => new UrlInfo(text, false, culture);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((UrlInfo) obj);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (Culture != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Culture) : 0);
hashCode = (hashCode * 397) ^ IsUrl.GetHashCode();
hashCode = (hashCode * 397) ^ (Text != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Text) : 0);
return hashCode;
}
}
public static bool operator ==(UrlInfo left, UrlInfo right)
{
return Equals(left, right);
}
public static bool operator !=(UrlInfo left, UrlInfo right)
{
return !Equals(left, right);
}
}
}
+5 -8
View File
@@ -202,7 +202,7 @@ namespace Umbraco.Web.Routing
var url = _urlProviders.Select(provider => provider.GetUrl(_umbracoContext, content, mode, culture, current))
.FirstOrDefault(u => u != null);
return url ?? "#"; // legacy wants this
return url?.Text ?? "#"; // legacy wants this
}
internal string GetUrlFromRoute(int id, string route, string culture)
@@ -210,7 +210,7 @@ namespace Umbraco.Web.Routing
var provider = _urlProviders.OfType<DefaultUrlProvider>().FirstOrDefault();
var url = provider == null
? route // what else?
: provider.GetUrlFromRoute(route, UmbracoContext.Current, id, _umbracoContext.CleanedUmbracoUrl, Mode, culture);
: provider.GetUrlFromRoute(route, UmbracoContext.Current, id, _umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text;
return url ?? "#";
}
@@ -228,7 +228,7 @@ namespace Umbraco.Web.Routing
/// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// <para>The results depend on the current url.</para>
/// </remarks>
public IEnumerable<string> GetOtherUrls(int id)
public IEnumerable<UrlInfo> GetOtherUrls(int id)
{
return GetOtherUrls(id, _umbracoContext.CleanedUmbracoUrl);
}
@@ -243,12 +243,9 @@ namespace Umbraco.Web.Routing
/// <para>Other urls are those that <c>GetUrl</c> would not return in the current context, but would be valid
/// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...).</para>
/// </remarks>
public IEnumerable<string> GetOtherUrls(int id, Uri current)
public IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
{
// providers can return null or an empty list or a non-empty list, be prepared
var urls = _urlProviders.SelectMany(provider => provider.GetOtherUrls(_umbracoContext, id, current) ?? Enumerable.Empty<string>());
return urls;
return _urlProviders.SelectMany(provider => provider.GetOtherUrls(_umbracoContext, id, current));
}
#endregion
@@ -5,7 +5,6 @@ using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core;
using Umbraco.Core.Logging;
using LightInject;
namespace Umbraco.Web.Routing
{
@@ -18,7 +17,7 @@ namespace Umbraco.Web.Routing
/// <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,
public static IEnumerable<UrlInfo> GetContentUrls(this IContent content,
PublishedRouter publishedRouter,
UmbracoContext umbracoContext,
ILocalizationService localizationService,
@@ -34,25 +33,71 @@ namespace Umbraco.Web.Routing
if (contentService == null) throw new ArgumentNullException(nameof(contentService));
if (logger == null) throw new ArgumentNullException(nameof(logger));
var urls = new List<UrlInfo>();
if (content.Published == false)
{
urls.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished")));
return urls;
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 on each culture.
// on invariant trees, each culture return the same thing
// but, we don't know if the tree to this content is invariant
// 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, logger))
{
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 umbracoContext.UrlProvider.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,
PublishedRouter publishedRouter,
UmbracoContext umbracoContext,
IContentService contentService,
ILocalizedTextService textService,
ILogger logger)
{
foreach (var culture in cultures)
{
// if content is variant, and culture is not published, skip
@@ -76,53 +121,26 @@ namespace Umbraco.Web.Routing
{
// deal with 'could not get the url'
case "#":
HandleCouldNotGetUrl(content, culture, urls, contentService, textService);
yield return HandleCouldNotGetUrl(content, culture, contentService, textService);
break;
// deal with exceptions
case "#ex":
urls.Add(UrlInfo.Message(textService.Localize("content/getUrlException"), culture));
yield return UrlInfo.Message(textService.Localize("content/getUrlException"), culture);
break;
// got a url, deal with collisions, add url
default:
if (!DetectCollision(content, url, urls, culture, umbracoContext, publishedRouter, textService)) // detect collisions, etc
urls.Add(UrlInfo.Url(url, culture));
if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc
yield return urlInfo;
else
yield return UrlInfo.Url(url, culture);
break;
}
}
// prepare for de-duplication
var durl = new Dictionary<string, List<UrlInfo>>();
var dmsg = new Dictionary<string, List<UrlInfo>>();
foreach (var url in urls)
{
var d = url.IsUrl ? durl : dmsg;
if (!d.TryGetValue(url.Text, out var l))
d[url.Text] = l = new List<UrlInfo>();
l.Add(url);
}
// deduplicate, order urls first then messages, concatenate cultures (hide if 'all')
var ret = new List<UrlInfo>();
foreach (var (text, infos) in durl)
ret.Add(UrlInfo.Url(text, infos.Count == cultures.Count ? null : string.Join(", ", infos.Select(x => x.Culture))));
foreach (var (text, infos) in dmsg)
ret.Add(UrlInfo.Message(text, infos.Count == cultures.Count ? null : string.Join(", ", infos.Select(x => x.Culture))));
// 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.
// also, we are not dealing with cultures at all - that will have to wait
foreach(var otherUrl in umbracoContext.UrlProvider.GetOtherUrls(content.Id))
{
if (urls.Any(x => x.IsUrl && x.Text == otherUrl)) continue;
ret.Add(UrlInfo.Url(otherUrl));
}
return ret;
}
private static void HandleCouldNotGetUrl(IContent content, string culture, List<UrlInfo> urls, IContentService contentService, ILocalizedTextService textService)
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.
@@ -134,16 +152,16 @@ namespace Umbraco.Web.Routing
while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
if (parent == null) // oops, internal error
urls.Add(UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture));
return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture);
else if (!parent.Published) // totally not published
urls.Add(UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] { parent.Name }), culture));
return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] {parent.Name}), culture);
else // culture not published
urls.Add(UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] { parent.Name }), culture));
return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture);
}
private static bool DetectCollision(IContent content, string url, List<UrlInfo> urls, string culture, UmbracoContext umbracoContext, PublishedRouter publishedRouter, ILocalizedTextService textService)
private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, PublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo)
{
// test for collisions on the 'main' url
var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute);
@@ -152,9 +170,11 @@ namespace Umbraco.Web.Routing
var pcr = publishedRouter.CreateRequest(umbracoContext, uri);
publishedRouter.TryRouteRequest(pcr);
urlInfo = null;
if (pcr.HasPublishedContent == false)
{
urls.Add(UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture));
urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture);
return true;
}
@@ -173,7 +193,7 @@ namespace Umbraco.Web.Routing
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
urls.Add(UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture));
urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture);
return true;
}