diff --git a/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs b/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs index ed720e8bc1..ca65e4b9a1 100644 --- a/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs @@ -39,12 +39,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("url")] [NullSetting(NullSetting = NullSettings.NotNull)] - //[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "url, createDateUtc")] public string Url { get; set; } [Column("urlHash")] [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, createDateUtc")] + [Length(40)] public string UrlHash { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs index ce6af9186c..508c0f284b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs @@ -39,7 +39,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer .WithColumn("createDateUtc").AsDateTime().NotNullable() .WithColumn("url").AsString(2048).NotNullable() .WithColumn("contentKey").AsGuid().NotNullable() - .WithColumn("urlHash").AsString(20).NotNullable(); + .WithColumn("urlHash").AsString(40).NotNullable(); localContext.Create.Index("IX_" + umbracoRedirectUrlTableName).OnTable(umbracoRedirectUrlTableName) .OnColumn("urlHash") diff --git a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs index 2c608592b8..7073aae560 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs @@ -105,7 +105,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); ContentKey = redirectUrl.ContentKey, CreateDateUtc = redirectUrl.CreateDateUtc, Url = redirectUrl.Url, - UrlHash = HashUrl(redirectUrl.Url) + UrlHash = redirectUrl.Url.ToSHA1() }; } @@ -132,7 +132,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl Get(string url, Guid contentKey) { - var urlHash = HashUrl(url); + var urlHash = url.ToSHA1(); var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); @@ -155,7 +155,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl GetMostRecentUrl(string url) { - var urlHash = HashUrl(url); + var urlHash = url.ToSHA1(); var sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash) .OrderByDescending(x => x.CreateDateUtc, SqlSyntax); @@ -192,16 +192,6 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); var rules = result.Items.Select(Map); return rules; - } - - private static string HashUrl(string url) - { - using (var crypto = new SHA1CryptoServiceProvider()) - { - var inputBytes = Encoding.UTF8.GetBytes(url); - var hashedBytes = crypto.ComputeHash(inputBytes); - return Encoding.UTF8.GetString(hashedBytes); - } - } + } } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 766e038588..036b5b979f 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -731,6 +731,36 @@ namespace Umbraco.Core return stringBuilder.ToString(); } + /// + /// Converts the string to SHA1 + /// + /// referrs to itself + /// the md5 hashed string + public static string ToSHA1(this string stringToConvert) + { + //create an instance of the SHA1CryptoServiceProvider + var md5Provider = new SHA1CryptoServiceProvider(); + + //convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(stringToConvert); + + //get the hashed values created by our SHA1CryptoServiceProvider + var hashedByteArray = md5Provider.ComputeHash(byteArray); + + //create a StringBuilder object + var stringBuilder = new StringBuilder(); + + //loop to each each byte + foreach (var b in hashedByteArray) + { + //append it to our StringBuilder + stringBuilder.Append(b.ToString("x2").ToLower()); + } + + //return the hashed value + return stringBuilder.ToString(); + } + /// /// Decodes a string that was encoded with UrlTokenEncode diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index dc3f4243e7..c3ef596ad0 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -26,5 +26,9 @@ namespace Umbraco.Core.Strings /// per content, in 1-to-1 multilingual configurations. Then there would be one /// url per culture. string GetUrlSegment(IContentBase content, CultureInfo culture); + + //TODO: For the 301 tracking, we need to add another extended interface to this so that + // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. + // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing } } diff --git a/src/Umbraco.Web/Redirects/RedirectTrackingEventHandler.cs b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs similarity index 54% rename from src/Umbraco.Web/Redirects/RedirectTrackingEventHandler.cs rename to src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs index 417238bdbe..b0340fa7e4 100644 --- a/src/Umbraco.Web/Redirects/RedirectTrackingEventHandler.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs @@ -1,18 +1,20 @@ using System; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Publishing; -using Umbraco.Core.Events; -using Umbraco.Web.Routing; using System.Collections.Generic; using System.Linq; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Web.Cache; +using Umbraco.Web.PublishedCache; -namespace Umbraco.Web.Redirects +namespace Umbraco.Web.Routing { /// /// Implements an Application Event Handler for managing redirect urls tracking. @@ -24,9 +26,9 @@ namespace Umbraco.Web.Redirects /// public class RedirectTrackingEventHandler : ApplicationEventHandler { - 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"; + private const string ContextKey1 = "Umbraco.Web.Routing.RedirectTrackingEventHandler.1"; + private const string ContextKey2 = "Umbraco.Web.Routing.RedirectTrackingEventHandler.2"; + private const string ContextKey3 = "Umbraco.Web.Routing.RedirectTrackingEventHandler.3"; /// protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) @@ -89,62 +91,98 @@ namespace Umbraco.Web.Redirects // rolled back items have to be published, so publishing will take care of that } + /// + /// Tracks a documents URLs during publishing in the current request + /// private static Dictionary> OldRoutes { get { - if (UmbracoContext.Current == null) - return null; - var oldRoutes = (Dictionary>) UmbracoContext.Current.HttpContext.Items[ContextKey3]; - if (oldRoutes == null) - UmbracoContext.Current.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary>(); + var oldRoutes = RequestCache.GetCacheItem>>( + ContextKey3, + () => new Dictionary>()); return oldRoutes; } } private static bool LockedEvents { - get { return Moving && UmbracoContext.Current.HttpContext.Items[ContextKey2] != null; } + get + { + return Moving && RequestCache.GetCacheItem(ContextKey2) != null; + } set { if (Moving && value) - UmbracoContext.Current.HttpContext.Items[ContextKey2] = true; + { + //this forces true into the cache + RequestCache.GetCacheItem(ContextKey2, () => true); + } else - UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2); + { + RequestCache.ClearCacheItem(ContextKey2); + } } } private static bool Moving { - get { return UmbracoContext.Current.HttpContext.Items[ContextKey1] != null; } + get { return RequestCache.GetCacheItem(ContextKey1) != null; } set { if (value) - UmbracoContext.Current.HttpContext.Items[ContextKey1] = true; + { + //this forces true into the cache + RequestCache.GetCacheItem(ContextKey1, () => true); + } else { - UmbracoContext.Current.HttpContext.Items.Remove(ContextKey1); - UmbracoContext.Current.HttpContext.Items.Remove(ContextKey2); + RequestCache.ClearCacheItem(ContextKey1); + RequestCache.ClearCacheItem(ContextKey2); } } } + /// + /// Before the items are published, we need to get it's current URL before it changes + /// + /// + /// private static void ContentService_Publishing(IPublishingStrategy sender, PublishEventArgs args) { if (LockedEvents) return; - var contentCache = UmbracoContext.Current.ContentCache; + var contentCache = GetPublishedCache(); + if (contentCache == null) return; + foreach (var entity in args.PublishedEntities) { - var entityContent = contentCache.GetById(entity.Id); - if (entityContent == null) continue; - foreach (var x in entityContent.DescendantsOrSelf()) + //TODO: This is horrible - we need to check if the url segment for this entity is changing in + // order to determine if we need to make redirects for itself and all of it's descendents. + // The way this works right now (7.5.0) is that we re-lookup the entity that is currently being published which + // returns it's existing data in the db which we use to extract it's current segment. Then we compare that with + // the segment value returned from the current entity. + // In the future this will certainly cause some problems, to fix this we'd need to change the IUrlSegmentProvider + // to support being able to determine if a segment is going to change for an entity. See notes in IUrlSegmentProvider. + var oldEntity = ApplicationContext.Current.Services.ContentService.GetById(entity.Id); + if (oldEntity == null) continue; + var oldSegmentName = oldEntity.GetUrlSegment(); + + //if the segment has changed or we are moving, then process all descendent + // Urls and schedule them for creating a rewrite when publishing is done. + if (oldSegmentName != entity.GetUrlSegment() || Moving) { - var route = contentCache.GetRouteById(x.Id); - if (IsNotRoute(route)) continue; - var wk = UnwrapToKey(x); - if (wk == null) continue; - OldRoutes[x.Id] = Tuple.Create(wk.Key, route); + var entityContent = contentCache.GetById(entity.Id); + if (entityContent == null) continue; + foreach (var x in entityContent.DescendantsOrSelf()) + { + var route = contentCache.GetRouteById(x.Id); + if (IsNotRoute(route)) continue; + var wk = UnwrapToKey(x); + if (wk == null) continue; + + OldRoutes[x.Id] = Tuple.Create(wk.Key, route); + } } } @@ -165,23 +203,37 @@ namespace Umbraco.Web.Redirects return withKey; } + /// + /// Executed when the cache updates, which means we can know what the new URL is for a given document + /// + /// + /// private void PageCacheRefresher_CacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs cacheRefresherEventArgs) { - if (OldRoutes == null) - return; - - var removeKeys = new List(); - - foreach (var oldRoute in OldRoutes) + //This should only ever occur on the Master server when in load balancing since this will fire on all + // servers taking part in load balancing + var serverRole = ApplicationContext.Current.GetCurrentServerRole(); + if (serverRole == ServerRole.Master || serverRole == ServerRole.Single) { - // 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); - removeKeys.Add(oldRoute.Key); - } + //if the Old routes is empty do not continue + if (OldRoutes.Count == 0) + return; - foreach (var k in removeKeys) - OldRoutes.Remove(k); + try + { + 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); + } + } + finally + { + OldRoutes.Clear(); + RequestCache.ClearCacheItem(ContextKey3); + } + } } private static void ContentService_Published(IPublishingStrategy sender, PublishEventArgs e) @@ -203,7 +255,10 @@ namespace Umbraco.Web.Redirects private static void CreateRedirect(int contentId, Guid contentKey, string oldRoute) { - var contentCache = UmbracoContext.Current.ContentCache; + + var contentCache = GetPublishedCache(); + if (contentCache == null) return; + var newRoute = contentCache.GetRouteById(contentId); if (IsNotRoute(newRoute) || oldRoute == newRoute) return; var redirectUrlService = ApplicationContext.Current.Services.RedirectUrlService; @@ -216,5 +271,21 @@ namespace Umbraco.Web.Redirects // err/- if collision or anomaly or ... return route == null || route.StartsWith("err/"); } + + /// + /// Gets the current request cache to persist the values between handlers + /// + private static ContextualPublishedContentCache GetPublishedCache() + { + return UmbracoContext.Current == null ? null : UmbracoContext.Current.ContentCache; + } + + /// + /// Gets the current request cache to persist the values between handlers + /// + private static ICacheProvider RequestCache + { + get { return ApplicationContext.Current.ApplicationCache.RequestCache; } + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d1cab1b52d..41f0256cab 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -390,7 +390,7 @@ - +