#3539 - Re-enabled url redirect tracking and implemented support for culture variants

This commit is contained in:
Bjarke Berg
2019-01-04 11:21:46 +01:00
parent e1709eaed5
commit 9bf66b0881
16 changed files with 184 additions and 90 deletions

View File

@@ -119,6 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade
To<DropTemplateDesignColumn>("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
To<TablesForScheduledPublishing>("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
To<MakeTagsVariant>("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}");
To<MakeRedirectUrlVariant>("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}");
//FINAL

View File

@@ -0,0 +1,29 @@
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
public class MakeRedirectUrlVariant : MigrationBase
{
public MakeRedirectUrlVariant(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
AddColumn<RedirectUrlDto>("culture");
Delete.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl).Do();
Create.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl)
.OnColumn("urlHash")
.Ascending()
.OnColumn("contentKey")
.Ascending()
.OnColumn("culture")
.Ascending()
.OnColumn("createDateUtc")
.Ascending()
.WithOptions().Unique()
.Do();
}
}
}

View File

@@ -27,11 +27,18 @@ namespace Umbraco.Core.Models
[DataMember]
DateTime CreateDateUtc { get; set; }
/// <summary>
/// Gets or sets the culture.
/// </summary>
[DataMember]
string Culture { get; set; }
/// <summary>
/// Gets or sets the redirect url route.
/// </summary>
/// <remarks>Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.</remarks>
[DataMember]
string Url { get; set; }
}
}

View File

@@ -28,12 +28,14 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo ContentIdSelector = ExpressionHelper.GetPropertyInfo<RedirectUrl, int>(x => x.ContentId);
public readonly PropertyInfo ContentKeySelector = ExpressionHelper.GetPropertyInfo<RedirectUrl, Guid>(x => x.ContentKey);
public readonly PropertyInfo CreateDateUtcSelector = ExpressionHelper.GetPropertyInfo<RedirectUrl, DateTime>(x => x.CreateDateUtc);
public readonly PropertyInfo CultureSelector = ExpressionHelper.GetPropertyInfo<RedirectUrl, string>(x => x.Culture);
public readonly PropertyInfo UrlSelector = ExpressionHelper.GetPropertyInfo<RedirectUrl, string>(x => x.Url);
}
private int _contentId;
private Guid _contentKey;
private DateTime _createDateUtc;
private string _culture;
private string _url;
/// <inheritdoc />
@@ -57,6 +59,13 @@ namespace Umbraco.Core.Models
set { SetPropertyValueAndDetectChanges(value, ref _createDateUtc, Ps.Value.CreateDateUtcSelector); }
}
/// <inheritdoc />
public string Culture
{
get { return _culture; }
set { SetPropertyValueAndDetectChanges(value, ref _culture, Ps.Value.CultureSelector); }
}
/// <inheritdoc />
public string Url
{

View File

@@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Dtos
// notes
//
// we want a unique, non-clustered index on (url ASC, contentId ASC, createDate DESC) but the
// we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the
// problem is that the index key must be 900 bytes max. should we run without an index? done
// some perfs comparisons, and running with an index on a hash is only slightly slower on
// inserts, and much faster on reads, so... we have an index on a hash.
@@ -41,9 +41,13 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.NotNull)]
public string Url { get; set; }
[Column("culture")]
[NullSetting(NullSetting = NullSettings.Null)]
public string Culture { get; set; }
[Column("urlHash")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, createDateUtc")]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")]
[Length(40)]
public string UrlHash { get; set; }
}

View File

@@ -14,8 +14,9 @@ namespace Umbraco.Core.Persistence.Repositories
/// </summary>
/// <param name="url">The Umbraco redirect url route.</param>
/// <param name="contentKey">The content unique key.</param>
/// <param name="culture">The culture.</param>
/// <returns></returns>
IRedirectUrl Get(string url, Guid contentKey);
IRedirectUrl Get(string url, Guid contentKey, string culture);
/// <summary>
/// Deletes a redirect url.

View File

@@ -104,6 +104,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
ContentKey = redirectUrl.ContentKey,
CreateDateUtc = redirectUrl.CreateDateUtc,
Url = redirectUrl.Url,
Culture = redirectUrl.Culture,
UrlHash = redirectUrl.Url.ToSHA1()
};
}
@@ -121,6 +122,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
url.ContentId = dto.ContentId;
url.ContentKey = dto.ContentKey;
url.CreateDateUtc = dto.CreateDateUtc;
url.Culture = dto.Culture;
url.Url = dto.Url;
return url;
}
@@ -130,10 +132,10 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
}
}
public IRedirectUrl Get(string url, Guid contentKey)
public IRedirectUrl Get(string url, Guid contentKey, string culture)
{
var urlHash = url.ToSHA1();
var sql = GetBaseQuery(false).Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey);
var sql = GetBaseQuery(false).Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture);
var dto = Database.Fetch<RedirectUrlDto>(sql).FirstOrDefault();
return dto == null ? null : Map(dto);
}

View File

@@ -14,8 +14,9 @@ namespace Umbraco.Core.Services
/// </summary>
/// <param name="url">The Umbraco url route.</param>
/// <param name="contentKey">The content unique key.</param>
/// <param name="culture">The culture.</param>
/// <remarks>Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.</remarks>
void Register(string url, Guid contentKey);
void Register(string url, Guid contentKey, string culture = null);
/// <summary>
/// Deletes all redirect urls for a given content.

View File

@@ -19,15 +19,15 @@ namespace Umbraco.Core.Services.Implement
_redirectUrlRepository = redirectUrlRepository;
}
public void Register(string url, Guid contentKey)
public void Register(string url, Guid contentKey, string culture = null)
{
using (var scope = ScopeProvider.CreateScope())
{
var redir = _redirectUrlRepository.Get(url, contentKey);
var redir = _redirectUrlRepository.Get(url, contentKey, culture);
if (redir != null)
redir.CreateDateUtc = DateTime.UtcNow;
else
redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey };
redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture};
_redirectUrlRepository.Save(redir);
scope.Complete();
}

View File

@@ -367,6 +367,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\DropTemplateDesignColumn.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FixLockTablePrimaryKey.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\LanguageColumns.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\MakeRedirectUrlVariant.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\MakeTagsVariant.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\PropertyEditorsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorMacroColumns.cs" />

View File

@@ -54,9 +54,10 @@
<div class="umb-table-head">
<div class="umb-table-row">
<div class="umb-table-cell not-fixed flx-s1 flx-g1 flx-b4"><localize key="redirectUrls_originalUrl">Original URL</localize></div>
<div class="umb-table-cell not-fixed flx-s1 flx-g1 flx-b1"><localize key="redirectUrls_culture">Culture</localize></div>
<div class="umb-table-cell flx-s1 flx-g1 flx-b4"><localize key="redirectUrls_originalUrl">Original URL</localize></div>
<div class="umb-table-cell flx-s0 flx-g0" style="flex-basis: 20px;"></div>
<div class="umb-table-cell flx-s1 flx-g1 flx-b6"><localize key="redirectUrls_redirectedTo">Redirected To</localize></div>
<div class="umb-table-cell flx-s1 flx-g1 flx-b5"><localize key="redirectUrls_redirectedTo">Redirected To</localize></div>
</div>
</div>
@@ -64,8 +65,10 @@
<div class="umb-table-row -solid" ng-repeat="redirectUrl in vm.redirectUrls">
<div class="umb-table-cell not-fixed flx-s1 flx-g1 flx-b4">
<div class="umb-table-cell not-fixed flx-s1 flx-g1 flx-b1">
{{redirectUrl.culture ||'*'}}
</div>
<div class="umb-table-cell flx-s1 flx-g1 flx-b4">
<a class="umb-table-body__link" href="{{redirectUrl.originalUrl}}" target="_blank" title="{{redirectUrl.originalUrl}}">{{redirectUrl.originalUrl}}</a>
</div>
@@ -73,7 +76,7 @@
<i class="umb-table-body__icon umb-table-body__fileicon icon-arrow-right" style="font-size: 12px; line-height: 1;"></i>
</div>
<div class="umb-table-cell flx-s1 flx-g1 flx-b6 items-center">
<div class="umb-table-cell flx-s1 flx-g1 flx-b5 items-center">
<div class="flx-s1 flx-g1 flx-b0" style="margin-right: 20px;">
<a class="umb-table-body__link" href="{{redirectUrl.destinationUrl}}" target="_blank" title="{{redirectUrl.destinationUrl}}">{{redirectUrl.destinationUrl}}</a>
</div>

View File

@@ -1995,6 +1995,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<area alias="redirectUrls">
<key alias="disableUrlTracker">Disable URL tracker</key>
<key alias="enableUrlTracker">Enable URL tracker</key>
<key alias="culture">Culture</key>
<key alias="originalUrl">Original URL</key>
<key alias="redirectedTo">Redirected To</key>
<key alias="redirectUrlManagement">Redirect Url Management</key>

View File

@@ -51,12 +51,6 @@ namespace Umbraco.Web.Editors
: redirectUrlService.SearchRedirectUrls(searchTerm, page, pageSize, out resultCount);
searchResult.SearchResults = Mapper.Map<IEnumerable<ContentRedirectUrl>>(redirects).ToArray();
//now map the Content/published url
foreach (var result in searchResult.SearchResults)
{
result.DestinationUrl = result.ContentId > 0 ? Umbraco.Url(result.ContentId) : "#";
}
searchResult.TotalCount = resultCount;
searchResult.CurrentPage = page;
searchResult.PageCount = ((int)resultCount + pageSize - 1) / pageSize;

View File

@@ -20,5 +20,8 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "contentId")]
public int ContentId { get; set; }
[DataMember(Name = "culture")]
public string Culture { get; set; }
}
}

View File

@@ -8,18 +8,12 @@ namespace Umbraco.Web.Models.Mapping
{
internal class RedirectUrlMapperProfile : Profile
{
private readonly UrlProvider _urlProvider;
public RedirectUrlMapperProfile(UrlProvider urlProvider)
{
_urlProvider = urlProvider;
}
public RedirectUrlMapperProfile()
{
CreateMap<IRedirectUrl, ContentRedirectUrl>()
.ForMember(x => x.OriginalUrl, expression => expression.MapFrom(item => _urlProvider.GetUrlFromRoute(item.ContentId, item.Url, null)))
.ForMember(x => x.DestinationUrl, expression => expression.Ignore())
.ForMember(x => x.OriginalUrl, expression => expression.MapFrom(item => Current.UmbracoContext.UrlProvider.GetUrlFromRoute(item.ContentId, item.Url, item.Culture)))
.ForMember(x => x.DestinationUrl, expression => expression.MapFrom(item => item.ContentId > 0 ? new UmbracoHelper(Current.UmbracoContext, Current.Services).Url(item.ContentId, item.Culture) : "#"))
.ForMember(x => x.RedirectId, expression => expression.MapFrom(item => item.Key));
}
}

View File

@@ -1,36 +1,85 @@
using System;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Events;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Components;
using Umbraco.Core.Configuration;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Sync;
using Umbraco.Web.Cache;
using Umbraco.Web.Composing;
using Umbraco.Web.Routing;
namespace Umbraco.Web.Redirects
{
/// <summary>
/// Implements an Application Event Handler for managing redirect urls tracking.
/// Implements an Application Event Handler for managing redirect urls tracking.
/// </summary>
/// <remarks>
/// <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>
/// <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>
/// </remarks>
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
[DisableComponent] // fixme - re-enable when we fix redirect tracking with variants
public class RedirectTrackingComponent : UmbracoComponentBase, IUmbracoCoreComponent
{
private const string ContextKey1 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.1";
private const string ContextKey2 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.2";
private const string ContextKey3 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.3";
protected void Initialize()
private static Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute> OldRoutes
{
get
{
var oldRoutes =
(Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>) UmbracoContext.Current.HttpContext.Items[
ContextKey3];
if (oldRoutes == null)
UmbracoContext.Current.HttpContext.Items[ContextKey3] =
oldRoutes = new Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>();
return oldRoutes;
}
}
private static bool LockedEvents
{
get => Moving && UmbracoContext.Current.HttpContext.Items[ContextKey2] != null;
set
{
if (Moving && value)
UmbracoContext.Current.HttpContext.Items[ContextKey2] = true;
else
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2);
}
}
private static bool Moving
{
get => UmbracoContext.Current.HttpContext.Items[ContextKey1] != null;
set
{
if (value)
UmbracoContext.Current.HttpContext.Items[ContextKey1] = true;
else
{
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey1);
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2);
}
}
}
protected void Initialize(ContentFinderCollectionBuilder contentFinderCollectionBuilder)
{
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableRedirectUrlTracking) return;
// events are weird
// on 'published' we 'could' get the old or the new route depending on event handlers order
// so it is not reliable. getting the old route in 'publishing' to be sure and storing in http
@@ -50,6 +99,7 @@ namespace Umbraco.Web.Redirects
ContentService.Moved += ContentService_Moved;
ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated;
// kill all redirects once a content is deleted
//ContentService.Deleted += ContentService_Deleted;
// BUT, doing it here would prevent content deletion due to FK
@@ -58,69 +108,38 @@ namespace Umbraco.Web.Redirects
// rolled back items have to be published, so publishing will take care of that
}
private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args)
private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender,
CacheRefresherEventArgs args)
{
// sanity checks
if (args.MessageType != MessageType.RefreshByPayload)
{
throw new InvalidOperationException("ContentCacheRefresher MessageType should be ByPayload.");
if (args.MessageObject == null) return;
var payloads = args.MessageObject as ContentCacheRefresher.JsonPayload[];
if (payloads == null)
}
if (args.MessageObject == null)
{
return;
}
if (args.MessageObject is ContentCacheRefresher.JsonPayload[])
{
throw new InvalidOperationException("ContentCacheRefresher MessageObject should be JsonPayload[].");
}
// manage routes
var removeKeys = new List<int>();
var removeKeys = new List<ContentIdAndCulture>();
foreach (var oldRoute in OldRoutes)
{
// assuming we cannot have 'CacheUpdated' for only part of the infos else we'd need
// to set a flag in 'Published' to indicate which entities have been refreshed ok
CreateRedirect(oldRoute.Key, oldRoute.Value.Item1, oldRoute.Value.Item2);
CreateRedirect(oldRoute.Key.ContentId, oldRoute.Key.Culture, oldRoute.Value.ContentKey,
oldRoute.Value.OldRoute);
removeKeys.Add(oldRoute.Key);
}
foreach (var k in removeKeys)
{
OldRoutes.Remove(k);
}
private static Dictionary<int, Tuple<Guid, string>> OldRoutes
{
get
{
var oldRoutes = (Dictionary<int, Tuple<Guid, string>>) UmbracoContext.Current.HttpContext.Items[ContextKey3];
if (oldRoutes == null)
UmbracoContext.Current.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary<int, Tuple<Guid, string>>();
return oldRoutes;
}
}
private static bool LockedEvents
{
get { return Moving && UmbracoContext.Current.HttpContext.Items[ContextKey2] != null; }
set
{
if (Moving && value)
UmbracoContext.Current.HttpContext.Items[ContextKey2] = true;
else
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2);
}
}
private static bool Moving
{
get { return UmbracoContext.Current.HttpContext.Items[ContextKey1] != null; }
set
{
if (value)
UmbracoContext.Current.HttpContext.Items[ContextKey1] = true;
else
{
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey1);
UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2);
}
}
}
@@ -135,9 +154,14 @@ namespace Umbraco.Web.Redirects
if (entityContent == null) continue;
foreach (var x in entityContent.DescendantsOrSelf())
{
var route = contentCache.GetRouteById(x.Id);
if (IsNotRoute(route)) continue;
OldRoutes[x.Id] = Tuple.Create(x.Key, route);
var cultures = x.Cultures.Any() ? x.Cultures.Select(c => c.Key) : new[] {(string) null};
foreach (var culture in cultures)
{
var route = contentCache.GetRouteById(x.Id, culture);
if (IsNotRoute(route)) return;
OldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route);
}
}
}
@@ -162,13 +186,13 @@ namespace Umbraco.Web.Redirects
LockedEvents = false;
}
private static void CreateRedirect(int contentId, Guid contentKey, string oldRoute)
private static void CreateRedirect(int contentId, string culture, Guid contentKey, string oldRoute)
{
var contentCache = UmbracoContext.Current.ContentCache;
var newRoute = contentCache.GetRouteById(contentId);
var newRoute = contentCache.GetRouteById(contentId, culture);
if (IsNotRoute(newRoute) || oldRoute == newRoute) return;
var redirectUrlService = Current.Services.RedirectUrlService;
redirectUrlService.Register(oldRoute, contentKey);
redirectUrlService.Register(oldRoute, contentKey, culture);
}
private static bool IsNotRoute(string route)
@@ -177,5 +201,25 @@ namespace Umbraco.Web.Redirects
// 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;
}
}
}