diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index f7e1ee9921..a0b4191633 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -119,6 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); + To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs new file mode 100644 index 0000000000..2e366c7c14 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -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("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(); + } + } +} diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs index f3c65fe89c..e066881645 100644 --- a/src/Umbraco.Core/Models/IRedirectUrl.cs +++ b/src/Umbraco.Core/Models/IRedirectUrl.cs @@ -27,11 +27,18 @@ namespace Umbraco.Core.Models [DataMember] DateTime CreateDateUtc { get; set; } + /// + /// Gets or sets the culture. + /// + [DataMember] + string Culture { get; set; } + /// /// Gets or sets the redirect url route. /// /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. [DataMember] string Url { get; set; } + } } diff --git a/src/Umbraco.Core/Models/RedirectUrl.cs b/src/Umbraco.Core/Models/RedirectUrl.cs index 187d9fdd6e..55b799244e 100644 --- a/src/Umbraco.Core/Models/RedirectUrl.cs +++ b/src/Umbraco.Core/Models/RedirectUrl.cs @@ -28,12 +28,14 @@ namespace Umbraco.Core.Models public readonly PropertyInfo ContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentId); public readonly PropertyInfo ContentKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ContentKey); public readonly PropertyInfo CreateDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.CreateDateUtc); + public readonly PropertyInfo CultureSelector = ExpressionHelper.GetPropertyInfo(x => x.Culture); public readonly PropertyInfo UrlSelector = ExpressionHelper.GetPropertyInfo(x => x.Url); } private int _contentId; private Guid _contentKey; private DateTime _createDateUtc; + private string _culture; private string _url; /// @@ -57,6 +59,13 @@ namespace Umbraco.Core.Models set { SetPropertyValueAndDetectChanges(value, ref _createDateUtc, Ps.Value.CreateDateUtcSelector); } } + /// + public string Culture + { + get { return _culture; } + set { SetPropertyValueAndDetectChanges(value, ref _culture, Ps.Value.CultureSelector); } + } + /// public string Url { diff --git a/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs index b2bc990f6b..57e7138827 100644 --- a/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs @@ -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; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 47a56bb530..d05f4e007c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -14,8 +14,9 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The Umbraco redirect url route. /// The content unique key. + /// The culture. /// - IRedirectUrl Get(string url, Guid contentKey); + IRedirectUrl Get(string url, Guid contentKey, string culture); /// /// Deletes a redirect url. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 5ec7fd6f3c..3ca937ffcd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -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(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey); + var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); } diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index 62e59e910c..3bd9b6f2cf 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -14,8 +14,9 @@ namespace Umbraco.Core.Services /// /// The Umbraco url route. /// The content unique key. + /// The culture. /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. - void Register(string url, Guid contentKey); + void Register(string url, Guid contentKey, string culture = null); /// /// Deletes all redirect urls for a given content. diff --git a/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs b/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs index e9703bd85c..80816961fc 100644 --- a/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs @@ -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(); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8ef76ade37..c7cba10eea 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -367,6 +367,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html index 24b32815e5..837c24cb0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html @@ -54,9 +54,10 @@
-
Original URL
+
Culture
+
Original URL
-
Redirected To
+
Redirected To
@@ -64,8 +65,10 @@
- -
+
+ {{redirectUrl.culture ||'*'}} +
+ @@ -73,7 +76,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index dcf62becb5..a96abadbbe 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1995,6 +1995,7 @@ To manage your website, simply open the Umbraco back office and start adding con Disable URL tracker Enable URL tracker + Culture Original URL Redirected To Redirect Url Management diff --git a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs index 377b76f52a..1c5e437e12 100644 --- a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs @@ -51,12 +51,6 @@ namespace Umbraco.Web.Editors : redirectUrlService.SearchRedirectUrls(searchTerm, page, pageSize, out resultCount); searchResult.SearchResults = Mapper.Map>(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; diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentRedirectUrl.cs b/src/Umbraco.Web/Models/ContentEditing/ContentRedirectUrl.cs index 805a6d60be..41fcb98c31 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentRedirectUrl.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentRedirectUrl.cs @@ -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; } } } diff --git a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs index 33e2164a21..53baa13379 100644 --- a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs @@ -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() - .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)); } } diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index 2558f18077..fea1126163 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -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 { /// - /// Implements an Application Event Handler for managing redirect urls tracking. + /// Implements an Application Event Handler for managing redirect urls tracking. /// /// - /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old url - /// not managing domains because we don't know how to do it - changing domains => must create a higher level strategy using rewriting rules probably - /// recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same + /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old url + /// + /// not managing domains because we don't know how to do it - changing domains => must create a higher level + /// strategy using rewriting rules probably + /// + /// recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same /// [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 OldRoutes { + get + { + var oldRoutes = + (Dictionary) UmbracoContext.Current.HttpContext.Items[ + ContextKey3]; + if (oldRoutes == null) + UmbracoContext.Current.HttpContext.Items[ContextKey3] = + oldRoutes = new Dictionary(); + 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(); + var removeKeys = new List(); 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> OldRoutes - { - get - { - var oldRoutes = (Dictionary>) UmbracoContext.Current.HttpContext.Items[ContextKey3]; - if (oldRoutes == null) - UmbracoContext.Current.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary>(); - 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 + { + public ContentIdAndCulture(int contentId, string culture) : base(contentId, culture) + { + } + + public int ContentId => Item1; + public string Culture => Item2; + } + + private class ContentKeyAndOldRoute : Tuple + { + public ContentKeyAndOldRoute(Guid contentKey, string oldRoute) : base(contentKey, oldRoute) + { + } + + public Guid ContentKey => Item1; + public string OldRoute => Item2; + } } }