Content Url and Audit (in progress)

This commit is contained in:
Stephan
2018-07-05 17:14:11 +02:00
parent e9a31437e1
commit fdfdd541e3
12 changed files with 250 additions and 160 deletions

View File

@@ -891,7 +891,7 @@ namespace Umbraco.Core.Services.Implement
}
var changeType = TreeChangeTypes.RefreshNode;
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
Audit(AuditType.Save, "Save Content performed by user", userId, content.Id);
Audit(AuditType.Save, "Saved by user", userId, content.Id);
scope.Complete();
}
@@ -931,7 +931,7 @@ namespace Umbraco.Core.Services.Implement
scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
}
scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs());
Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root);
Audit(AuditType.Save, "Bulk-saved by user", userId == -1 ? 0 : userId, Constants.System.Root);
scope.Complete();
}
@@ -988,6 +988,8 @@ namespace Umbraco.Core.Services.Implement
{
var evtMsgs = EventMessagesFactory.Get();
culture = culture.NullOrWhiteSpaceAsNull();
var publishedState = content.PublishedState;
if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method.");
@@ -996,12 +998,12 @@ namespace Umbraco.Core.Services.Implement
// cannot accept a specific culture for invariant content type (but '*' is ok)
if (content.ContentType.VariesByCulture())
{
if (culture.IsNullOrWhiteSpace())
if (culture == null)
throw new NotSupportedException("Invariant culture is not supported by variant content types.");
}
else
{
if (!culture.IsNullOrWhiteSpace() && culture != "*")
if (culture != null && culture != "*")
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
}
@@ -1010,7 +1012,7 @@ namespace Umbraco.Core.Services.Implement
return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content);
// all cultures = unpublish whole
if (culture == "*")
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
{
((Content) content).PublishedState = PublishedState.Unpublishing;
}
@@ -1031,9 +1033,21 @@ namespace Umbraco.Core.Services.Implement
var saved = SavePublishing(content, userId);
if (saved.Success)
{
Audit(AuditType.UnPublish, $"Unpublish variation culture: \"{culture ?? string.Empty}\" performed by user", userId, content.Id);
UnpublishResultType result;
if (culture == "*" || culture == null)
{
Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id);
result = UnpublishResultType.Success;
}
else
{
Audit(AuditType.UnPublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id);
if (!content.Published)
Audit(AuditType.UnPublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id);
result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture;
}
scope.Complete();
return new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content);
return new UnpublishResult(result, evtMsgs, content);
}
// failed - map result
@@ -1156,7 +1170,7 @@ namespace Umbraco.Core.Services.Implement
// events and audit
scope.Events.Dispatch(UnPublished, this, new PublishEventArgs<IContent>(content, false, false), "UnPublished");
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id);
Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id);
scope.Complete();
return new PublishResult(PublishResultType.Success, evtMsgs, content);
}
@@ -1187,7 +1201,7 @@ namespace Umbraco.Core.Services.Implement
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(descendants, false, false), "Published");
}
Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id);
Audit(AuditType.Publish, "Published by user", userId, content.Id);
scope.Complete();
return publishResult;
}
@@ -1321,7 +1335,7 @@ namespace Umbraco.Core.Services.Implement
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(document, TreeChangeTypes.RefreshBranch).ToEventArgs());
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(publishedDocuments, false, false), "Published");
Audit(AuditType.Publish, "SaveAndPublishBranch performed by user", userId, document.Id);
Audit(AuditType.Publish, "Branch published by user", userId, document.Id);
scope.Complete();
}
@@ -1390,7 +1404,7 @@ namespace Umbraco.Core.Services.Implement
DeleteLocked(scope, content);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.Remove).ToEventArgs());
Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id);
Audit(AuditType.Delete, "Deleted by user", userId, content.Id);
scope.Complete();
}
@@ -1458,7 +1472,7 @@ namespace Umbraco.Core.Services.Implement
deleteRevisionsEventArgs.CanCancel = false;
scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs);
Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root);
Audit(AuditType.Delete, "Delete (by version date) by user", userId, Constants.System.Root);
scope.Complete();
}
@@ -1495,7 +1509,7 @@ namespace Umbraco.Core.Services.Implement
_documentRepository.DeleteVersion(versionId);
scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId));
Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root);
Audit(AuditType.Delete, "Delete (by version) by user", userId, Constants.System.Root);
scope.Complete();
}
@@ -1540,7 +1554,7 @@ namespace Umbraco.Core.Services.Implement
moveEventArgs.CanCancel = false;
moveEventArgs.MoveInfoCollection = moveInfo;
scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed));
Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id);
Audit(AuditType.Move, "Moved to Recycle Bin by user", userId, content.Id);
scope.Complete();
}
@@ -1612,7 +1626,7 @@ namespace Umbraco.Core.Services.Implement
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved));
Audit(AuditType.Move, "Move Content performed by user", userId, content.Id);
Audit(AuditType.Move, "Moved by user", userId, content.Id);
scope.Complete();
}
@@ -1709,7 +1723,7 @@ namespace Umbraco.Core.Services.Implement
recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?!
scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs);
scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange<IContent>(x, TreeChangeTypes.Remove)).ToEventArgs());
Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent);
Audit(AuditType.Delete, "Recycle Bin emptied by user", 0, Constants.System.RecycleBinContent);
scope.Complete();
}

View File

@@ -18,7 +18,7 @@
/// <summary>
/// The specified variant was unpublished, the content item itself remains published.
/// </summary>
SuccessVariant = 2,
SuccessCulture = 2,
/// <summary>
/// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished

View File

@@ -1,19 +1,22 @@
<div class="umb-package-details">
<div class="umb-package-details__main-content">
<umb-box 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">
<a ng-if="node.published" href="{{url}}" target="_blank">
<i class="icon icon-window-popin"></i>
<span>{{url}}</span>
</a>
<div ng-if="!node.published">
<i class="icon icon-window-popin"></i>
<span>{{url}}</span>
<span ng-if="url.isUrl">
<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>
</div>
</li>
</ul>
@@ -23,12 +26,12 @@
<umb-box data-element="node-info-history">
<umb-box-header title-key="general_history"></umb-box-header>
<umb-box-content class="block-form">
<div style="position: relative;">
<div ng-if="loadingAuditTrail" style="background: rgba(255, 255, 255, 0.8); position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
<umb-load-indicator ng-if="loadingAuditTrail"></umb-load-indicator>
<div ng-if="auditTrail.length === 0" style="padding: 10px;">
<umb-empty-state
position="center"
@@ -39,10 +42,10 @@
<div class="history">
<div ng-if="auditTrail.length > 1" class="history-line"></div>
<div ng-if="auditTrail.length > 1" class="history-line"></div>
<div class="history-item" ng-repeat="item in auditTrail">
<div class="history-item__break">
<div class="history-item__avatar">
<umb-avatar
@@ -59,7 +62,7 @@
<div class="history-item__date">{{item.timestampFormatted}}</div>
</div>
</div>
<div class="history-item__break">
<umb-badge
style="margin-right: 5px;"
@@ -69,7 +72,7 @@
</umb-badge>
<span>{{ item.comment }}</span>
</div>
</div>
</div>
@@ -84,8 +87,8 @@
</umb-pagination>
</div>
</umb-box-content>
</umb-box>
</umb-box-content>
</umb-box>
</div>
<div class="umb-package-details__sidebar">
@@ -96,15 +99,15 @@
<div class="date-wrapper">
<div class="flex items-center flex-column">
<umb-date-time-picker
data-element="node-info-publish"
options="datePickerConfig"
on-change="datePickerChange(event, 'publish')">
<div class="date-container">
<div class="date-container__title">
<localize key="content_releaseDate"></localize>
</div>
@@ -115,12 +118,12 @@
<div class="date-wrapper__date">{{node.releaseDateDay}} {{node.releaseDateTime}}</div>
</div>
<a href="" ng-if="!node.releaseDate" class="bold" style="color: #00aea2; text-decoration: underline;"><localize key="content_setDate">Set date</localize></a>
<a href="" ng-if="!node.releaseDate" class="bold" style="color: #00aea2; text-decoration: underline;"><localize key="content_setDate">Set date</localize></a>
</div>
</umb-date-time-picker>
<a ng-if="node.releaseDate" ng-click="clearPublishDate()" href="" style="text-decoration: underline;">
<small><localize key="content_removeDate">Clear date</localize></small>
</a>
@@ -140,7 +143,7 @@
<div class="date-container__title">
<localize key="content_unpublishDate"></localize>
</div>
<div class="date-container__date" ng-if="node.removeDate">
<div class="date-wrapper__date">{{node.removeDateMonth}} {{node.removeDateYear}}</div>
<div class="date-wrapper__number">{{node.removeDateDayNumber}}</div>
@@ -159,7 +162,7 @@
</div>
</div>
</umb-box-content>
</umb-box>
@@ -200,7 +203,7 @@
<small>{{ node.key }}</small>
</umb-control-group>
</umb-box-content>
</umb-box-content>
</umb-box>
</div>
</div>

View File

@@ -5,16 +5,16 @@
<umb-box-content class="block-form">
<umb-empty-state
ng-if="!nodeUrl"
ng-if="!nodeUrl || !nodeUrl.isUrl"
size="small">
<localize key="content_noMediaLink"></localize>
</umb-empty-state>
<ul ng-if="nodeUrl" class="nav nav-stacked" style="margin-bottom: 0;">
<ul ng-if="nodeUrl && nodeUrl.isUrl" class="nav nav-stacked" style="margin-bottom: 0;">
<li>
<a href="{{nodeUrl}}" target="_blank">
<a href="{{nodeUrl.text}}" target="_blank">
<i class="icon icon-window-popin"></i>
<span>{{nodeUrl}}</span>
<span>{{nodeUrl.text}}</span>
</a>
</li>
</ul>
@@ -56,4 +56,4 @@
</umb-box>
</div>
</div>
</div>

View File

@@ -165,9 +165,11 @@
<key alias="noMediaLink">This media item has no link</key>
<key alias="otherElements">Properties</key>
<key alias="parentNotPublished">This document is published but is not visible because the parent '%0%' is unpublished</key>
<key alias="parentCultureNotPublished">This culture is published but is not visible because it is unpublished on parent '%0%'</key>
<key alias="parentNotPublishedAnomaly">This document is published but is not in the cache</key>
<key alias="getUrlException">Could not get the url</key>
<key alias="routeError">This document is published but its url would collide with content %0%</key>
<key alias="routeErrorCannotRoute">This document is published but its url cannot be routed</key>
<key alias="publish">Publish</key>
<key alias="published">Published</key>
<key alias="publishedPendingChanges">Published (pending changes)</key>

View File

@@ -199,9 +199,11 @@
<key alias="noMediaLink">This media item has no link</key>
<key alias="otherElements">Properties</key>
<key alias="parentNotPublished">This document is published but is not visible because the parent '%0%' is unpublished</key>
<key alias="parentCultureNotPublished">This culture is published but is not visible because it is unpublished on parent '%0%'</key>
<key alias="parentNotPublishedAnomaly">This document is published but is not in the cache</key>
<key alias="getUrlException">Could not get the url</key>
<key alias="routeError">This document is published but its url would collide with content %0%</key>
<key alias="routeErrorCannotRoute">This document is published but its url cannot be routed</key>
<key alias="publish">Publish</key>
<key alias="published">Published</key>
<key alias="publishedPendingChanges">Published (pending changes)</key>

View File

@@ -1003,7 +1003,7 @@ namespace Umbraco.Web.Editors
if (foundContent == null)
HandleContentNotFound(id);
var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0));
var content = MapToDisplay(foundContent, culture);
@@ -1019,7 +1019,7 @@ namespace Umbraco.Web.Editors
content.AddSuccessNotification(
Services.TextService.Localize("content/unPublish"),
unpublishResult.Result == UnpublishResultType.SuccessVariant
unpublishResult.Result == UnpublishResultType.SuccessCulture
? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture })
: Services.TextService.Localize("speechBubbles/contentUnpublished"));
@@ -1288,6 +1288,5 @@ namespace Umbraco.Web.Editors
return display;
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Models;
using Umbraco.Web.Routing;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -41,7 +42,7 @@ namespace Umbraco.Web.Models.ContentEditing
public ContentTypeBasic DocumentType { get; set; }
[DataMember(Name = "urls")]
public string[] Urls { get; set; }
public UrlInfo[] Urls { get; set; }
/// <summary>
/// Determines whether previewing is allowed for this node

View File

@@ -8,7 +8,7 @@ using Umbraco.Web.Routing;
namespace Umbraco.Web.Models.Mapping
{
internal class ContentUrlResolver : IValueResolver<IContent, ContentItemDisplay, string[]>
internal class ContentUrlResolver : IValueResolver<IContent, ContentItemDisplay, UrlInfo[]>
{
private readonly ILocalizedTextService _textService;
private readonly IContentService _contentService;
@@ -21,12 +21,12 @@ namespace Umbraco.Web.Models.Mapping
_logger = logger;
}
public string[] Resolve(IContent source, ContentItemDisplay destination, string[] destMember, ResolutionContext context)
public UrlInfo[] Resolve(IContent source, ContentItemDisplay destination, UrlInfo[] destMember, ResolutionContext context)
{
var umbracoContext = context.GetUmbracoContext(throwIfMissing: false);
var urls = umbracoContext == null
? new[] {"Cannot generate urls without a current Umbraco Context"}
? new[] { UrlInfo.Message("Cannot generate urls without a current Umbraco Context") }
: source.GetContentUrls(umbracoContext.UrlProvider, _textService, _contentService, _logger).ToArray();
return urls;

View File

@@ -0,0 +1,50 @@
using System.Runtime.Serialization;
namespace Umbraco.Web.Routing
{
/// <summary>
/// Represents infos for a url.
/// </summary>
[DataContract(Name = "urlInfo", Namespace = "")]
public class UrlInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="UrlInfo"/> class.
/// </summary>
private UrlInfo(string text, bool isUrl, string culture)
{
IsUrl = isUrl;
Text = text;
Culture = culture;
}
/// <summary>
/// Gets the culture.
/// </summary>
[DataMember(Name = "culture")]
public string Culture { get; }
/// <summary>
/// Gets a value indicating whether the url is a true url.
/// </summary>
/// <remarks>Otherwise, it is a message.</remarks>
[DataMember(Name = "isUrl")]
public bool IsUrl { get; }
/// <summary>
/// Gets the text, which is either the url, or a message.
/// </summary>
[DataMember(Name = "text")]
public string Text { get; }
/// <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);
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core;
@@ -18,7 +18,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<string> GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger)
public static IEnumerable<UrlInfo> GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger)
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider));
@@ -26,50 +26,39 @@ namespace Umbraco.Web.Routing
if (contentService == null) throw new ArgumentNullException(nameof(contentService));
if (logger == null) throw new ArgumentNullException(nameof(logger));
var urls = new HashSet<string>();
// going to build a list of urls (essentially for the back-office)
// which will contain
// - the 'main' url, which is what .Url would return, in the current culture
// - the 'other' urls we know (based upon domains, etc)
//
// this essentially happens when producing the urls for the back-office, and then we don't have
// a meaningful 'current culture' - so we need to explicitely pass some culture where required,
// and deal with whatever might happen
//
// if content is variant, go with the current culture - and that is NOT safe, there may be
// no 'main' url for that culture, deal with it later - otherwise, go with the invariant
// culture, and that is safe.
var varies = content.ContentType.VariesByCulture();
var culture = varies ? Thread.CurrentThread.CurrentUICulture.Name : "";
var urls = new List<UrlInfo>();
if (content.Published == false)
{
urls.Add(textService.Localize("content/itemNotPublished"));
urls.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished")));
return urls;
}
string url = null;
// fixme inject
// fixme PublishedRouter is stateless and should be a singleton!
var localizationService = Core.Composing.Current.Services.LocalizationService;
var publishedRouter = Core.Composing.Current.Container.GetInstance<PublishedRouter>();
if (varies)
{
if (!content.IsCulturePublished(culture))
{
urls.Add(textService.Localize("content/itemCultureNotPublished", culture));
// but keep going, we want to add the 'other' urls
url = "#no";
}
else if (!content.IsCultureAvailable(culture))
{
urls.Add(textService.Localize("content/itemCultureNotAvailable", culture));
// but keep going, we want to add the 'other' urls
url = "#no";
}
}
// 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
// get the 'main' url
if (url == null)
var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList();
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 = urlProvider.GetUrl(content.Id, culture);
@@ -79,79 +68,108 @@ namespace Umbraco.Web.Routing
logger.Error<UrlProvider>("GetUrl exception.", e);
url = "#ex";
}
}
if (url == "#") // deal with 'could not get the url'
{
// document as 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
switch (url)
{
parent = parent.ParentId > 0 ? parent.Parent(contentService) : null;
}
while (parent != null && parent.Published && (!varies || parent.IsCulturePublished(culture)));
// deal with 'could not get the url'
case "#":
HandleCouldNotGetUrl(content, culture, urls, contentService, textService);
break;
urls.Add(parent == null
? textService.Localize("content/parentNotPublishedAnomaly") // oops - internal error
: textService.Localize("content/parentNotPublished", new[] { parent.Name }));
}
else if (url == "#ex") // deal with exceptions
{
urls.Add(textService.Localize("content/getUrlException"));
}
else if (url == "#no") // deal with 'there is no main url'
{
// get the 'other' urls
foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id))
urls.Add(otherUrl);
}
else // detect collisions, etc
{
// test for collisions on the 'main' url
var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute);
if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl);
uri = UriUtility.UriToUmbraco(uri);
var r = Core.Composing.Current.Container.GetInstance<PublishedRouter>(); // fixme inject or ?
var pcr = r.CreateRequest(UmbracoContext.Current, uri);
r.TryRouteRequest(pcr);
// deal with exceptions
case "#ex":
urls.Add(UrlInfo.Message(textService.Localize("content/getUrlException"), culture));
break;
if (pcr.HasPublishedContent == false)
{
urls.Add(textService.Localize("content/routeError", new[] { "(error)" }));
}
else if (pcr.PublishedContent.Id != content.Id)
{
var o = pcr.PublishedContent;
string s;
if (o == null)
{
s = "(unknown)";
}
else
{
var l = new List<string>();
while (o != null)
{
l.Add(o.Name);
o = o.Parent;
}
l.Reverse();
s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
}
urls.Add(textService.Localize("content/routeError", s));
}
else
{
urls.Add(url);
// get the 'other' urls
foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id))
urls.Add(otherUrl);
// got a url, deal with collisions, add url
default:
if (!DetectCollision(content, url, urls, culture, publishedRouter, textService)) // detect collisions, etc
urls.Add(UrlInfo.Url(url, culture));
break;
}
}
return urls;
// 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))));
// fixme - need to add 'others' urls
// but, when?
//// get the 'other' urls
//foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id))
// urls.Add(otherUrl);
return ret;
}
private static void HandleCouldNotGetUrl(IContent content, string culture, List<UrlInfo> urls, 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 ? parent.Parent(contentService) : null;
}
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));
else if (!parent.Published) // totally not published
urls.Add(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));
}
private static bool DetectCollision(IContent content, string url, List<UrlInfo> urls, string culture, PublishedRouter publishedRouter, ILocalizedTextService textService)
{
// test for collisions on the 'main' url
var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute);
if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl);
uri = UriUtility.UriToUmbraco(uri);
var pcr = publishedRouter.CreateRequest(UmbracoContext.Current, uri);
publishedRouter.TryRouteRequest(pcr);
if (pcr.HasPublishedContent == false)
{
urls.Add(UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture));
return true;
}
if (pcr.PublishedContent.Id != content.Id)
{
var o = pcr.PublishedContent;
var l = new List<string>();
while (o != null)
{
l.Add(o.Name);
o = o.Parent;
}
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
urls.Add(UrlInfo.Message(textService.Localize("content/routeError", s), culture));
return true;
}
// no collision
return false;
}
}
}

View File

@@ -395,6 +395,7 @@
<Compile Include="Routing\ContentFinderCollectionBuilder.cs" />
<Compile Include="Routing\Domain.cs" />
<Compile Include="Routing\IContentLastChanceFinder.cs" />
<Compile Include="Routing\UrlInfo.cs" />
<Compile Include="Routing\UrlProviderCollection.cs" />
<Compile Include="Routing\UrlProviderCollectionBuilder.cs" />
<Compile Include="Scheduling\HealthCheckNotifier.cs" />