2016-06-12 16:54:13 +01:00
|
|
|
|
using System;
|
2016-06-12 20:52:49 +02:00
|
|
|
|
using System.Collections.Generic;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
using System.Linq;
|
2016-06-12 20:52:49 +02:00
|
|
|
|
using Umbraco.Core.Cache;
|
2019-02-14 09:15:47 +01:00
|
|
|
|
using Umbraco.Core.Composing;
|
2019-01-04 14:36:55 +01:00
|
|
|
|
using Umbraco.Core.Configuration.UmbracoSettings;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
using Umbraco.Core.Events;
|
|
|
|
|
|
using Umbraco.Core.Models;
|
|
|
|
|
|
using Umbraco.Core.Services;
|
2017-12-28 09:18:09 +01:00
|
|
|
|
using Umbraco.Core.Services.Implement;
|
2016-07-20 12:38:57 +02:00
|
|
|
|
using Umbraco.Core.Sync;
|
2016-06-12 20:52:49 +02:00
|
|
|
|
using Umbraco.Web.Cache;
|
2019-02-14 09:15:47 +01:00
|
|
|
|
using Current = Umbraco.Web.Composing.Current;
|
2016-06-12 16:54:13 +01:00
|
|
|
|
|
2019-01-29 23:05:59 +11:00
|
|
|
|
namespace Umbraco.Web.Routing
|
2016-06-12 16:54:13 +01:00
|
|
|
|
{
|
2019-01-04 14:36:55 +01:00
|
|
|
|
/// Implements an Application Event Handler for managing redirect urls tracking.
|
|
|
|
|
|
/// <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>
|
2019-01-04 08:36:38 +01:00
|
|
|
|
public sealed class RedirectTrackingComponent : IComponent
|
2016-06-12 16:54:13 +01:00
|
|
|
|
{
|
2016-06-12 20:52:49 +02:00
|
|
|
|
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";
|
|
|
|
|
|
|
2019-01-07 10:43:28 +01:00
|
|
|
|
private readonly IUmbracoSettingsSection _umbracoSettings;
|
|
|
|
|
|
|
|
|
|
|
|
public RedirectTrackingComponent(IUmbracoSettingsSection umbracoSettings)
|
|
|
|
|
|
{
|
|
|
|
|
|
_umbracoSettings = umbracoSettings;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-04 11:21:46 +01:00
|
|
|
|
private static Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute> OldRoutes
|
2016-06-12 20:52:49 +02:00
|
|
|
|
{
|
2019-01-04 11:21:46 +01:00
|
|
|
|
get
|
|
|
|
|
|
{
|
2019-02-14 09:49:45 +01:00
|
|
|
|
var oldRoutes = (Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>) Current.UmbracoContext.HttpContext.Items[ContextKey3];
|
2019-01-04 11:21:46 +01:00
|
|
|
|
if (oldRoutes == null)
|
2019-02-14 09:49:45 +01:00
|
|
|
|
Current.UmbracoContext.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>();
|
2019-01-04 11:21:46 +01:00
|
|
|
|
return oldRoutes;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-02-11 14:09:40 +01:00
|
|
|
|
private static bool HasOldRoutes
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Current.UmbracoContext == null) return false;
|
|
|
|
|
|
if (Current.UmbracoContext.HttpContext == null) return false;
|
|
|
|
|
|
if (Current.UmbracoContext.HttpContext.Items[ContextKey3] == null) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-04 11:21:46 +01:00
|
|
|
|
private static bool LockedEvents
|
|
|
|
|
|
{
|
2019-02-14 09:49:45 +01:00
|
|
|
|
get => Moving && Current.UmbracoContext.HttpContext.Items[ContextKey2] != null;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Moving && value)
|
2019-02-14 09:49:45 +01:00
|
|
|
|
Current.UmbracoContext.HttpContext.Items[ContextKey2] = true;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
else
|
2019-02-14 09:49:45 +01:00
|
|
|
|
Current.UmbracoContext.HttpContext.Items.Remove(ContextKey2);
|
2019-01-04 11:21:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool Moving
|
|
|
|
|
|
{
|
2019-02-14 09:49:45 +01:00
|
|
|
|
get => Current.UmbracoContext.HttpContext.Items[ContextKey1] != null;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value)
|
2019-02-14 09:49:45 +01:00
|
|
|
|
Current.UmbracoContext.HttpContext.Items[ContextKey1] = true;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2019-02-14 09:49:45 +01:00
|
|
|
|
Current.UmbracoContext.HttpContext.Items.Remove(ContextKey1);
|
|
|
|
|
|
Current.UmbracoContext.HttpContext.Items.Remove(ContextKey2);
|
2019-01-04 11:21:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-07 09:30:47 +01:00
|
|
|
|
public void Initialize()
|
2016-06-12 20:52:49 +02:00
|
|
|
|
{
|
2019-01-04 11:21:46 +01:00
|
|
|
|
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
|
2019-01-07 10:43:28 +01:00
|
|
|
|
if (_umbracoSettings.WebRouting.DisableRedirectUrlTracking) return;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
|
2016-06-12 20:52:49 +02:00
|
|
|
|
// 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
|
|
|
|
|
|
// context. then for the same reason, we have to process these old items only when the cache
|
|
|
|
|
|
// is ready
|
|
|
|
|
|
// when moving, the moved node is also published, which is causing all sorts of troubles with
|
|
|
|
|
|
// descendants, so when moving, we lock events so that neither 'published' nor 'publishing'
|
|
|
|
|
|
// are processed more than once
|
2016-07-20 12:38:57 +02:00
|
|
|
|
// we cannot rely only on ContentCacheRefresher because when CacheUpdated triggers the old
|
|
|
|
|
|
// route is gone
|
2016-06-12 20:52:49 +02:00
|
|
|
|
//
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// this is all very weird but it seems to work
|
2016-06-12 20:52:49 +02:00
|
|
|
|
|
|
|
|
|
|
ContentService.Publishing += ContentService_Publishing;
|
|
|
|
|
|
ContentService.Published += ContentService_Published;
|
2016-06-12 16:54:13 +01:00
|
|
|
|
ContentService.Moving += ContentService_Moving;
|
2016-06-12 20:52:49 +02:00
|
|
|
|
ContentService.Moved += ContentService_Moved;
|
2016-06-12 16:54:13 +01:00
|
|
|
|
|
2019-02-11 14:09:40 +01:00
|
|
|
|
ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
|
2016-06-12 20:52:49 +02:00
|
|
|
|
// kill all redirects once a content is deleted
|
|
|
|
|
|
//ContentService.Deleted += ContentService_Deleted;
|
|
|
|
|
|
// BUT, doing it here would prevent content deletion due to FK
|
|
|
|
|
|
// so the rows are actually deleted by the ContentRepository (see GetDeleteClauses)
|
2016-06-12 16:54:13 +01:00
|
|
|
|
|
2016-06-12 20:52:49 +02:00
|
|
|
|
// rolled back items have to be published, so publishing will take care of that
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
2016-06-12 20:52:49 +02:00
|
|
|
|
|
2019-01-07 09:30:47 +01:00
|
|
|
|
public void Terminate()
|
|
|
|
|
|
{ }
|
|
|
|
|
|
|
2019-02-11 14:09:40 +01:00
|
|
|
|
private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args)
|
2016-07-20 12:38:57 +02:00
|
|
|
|
{
|
2019-02-11 14:09:40 +01:00
|
|
|
|
// that event is a distributed even that triggers on all nodes
|
|
|
|
|
|
// BUT it should totally NOT run on nodes other that the one that handled the other events
|
|
|
|
|
|
// and besides, it cannot run on a background thread!
|
|
|
|
|
|
if (!HasOldRoutes)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2016-07-20 12:38:57 +02:00
|
|
|
|
// sanity checks
|
|
|
|
|
|
if (args.MessageType != MessageType.RefreshByPayload)
|
2019-01-04 11:21:46 +01:00
|
|
|
|
{
|
2016-07-20 12:38:57 +02:00
|
|
|
|
throw new InvalidOperationException("ContentCacheRefresher MessageType should be ByPayload.");
|
2019-01-04 11:21:46 +01:00
|
|
|
|
}
|
2019-02-11 14:09:40 +01:00
|
|
|
|
|
2019-01-04 11:21:46 +01:00
|
|
|
|
if (args.MessageObject == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2016-07-20 12:38:57 +02:00
|
|
|
|
|
2019-02-11 14:09:40 +01:00
|
|
|
|
if (!(args.MessageObject is ContentCacheRefresher.JsonPayload[]))
|
2019-01-04 11:21:46 +01:00
|
|
|
|
{
|
2016-07-20 12:38:57 +02:00
|
|
|
|
throw new InvalidOperationException("ContentCacheRefresher MessageObject should be JsonPayload[].");
|
2019-01-04 11:21:46 +01:00
|
|
|
|
}
|
2016-07-20 12:38:57 +02:00
|
|
|
|
|
|
|
|
|
|
// manage routes
|
2019-01-04 11:21:46 +01:00
|
|
|
|
var removeKeys = new List<ContentIdAndCulture>();
|
2016-07-20 12:38:57 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
2019-02-11 14:09:40 +01:00
|
|
|
|
CreateRedirect(oldRoute.Key.ContentId, oldRoute.Key.Culture, oldRoute.Value.ContentKey, oldRoute.Value.OldRoute);
|
2016-07-20 12:38:57 +02:00
|
|
|
|
removeKeys.Add(oldRoute.Key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var k in removeKeys)
|
2016-06-12 20:52:49 +02:00
|
|
|
|
{
|
2019-01-04 11:21:46 +01:00
|
|
|
|
OldRoutes.Remove(k);
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-06-12 20:52:49 +02:00
|
|
|
|
|
2016-07-08 17:58:01 +02:00
|
|
|
|
private static void ContentService_Publishing(IContentService sender, PublishEventArgs<IContent> args)
|
2016-06-12 16:54:13 +01:00
|
|
|
|
{
|
2016-06-12 20:52:49 +02:00
|
|
|
|
if (LockedEvents) return;
|
|
|
|
|
|
|
2019-04-22 18:14:03 +02:00
|
|
|
|
var contentCache = Current.UmbracoContext.Content;
|
2016-06-12 20:52:49 +02:00
|
|
|
|
foreach (var entity in args.PublishedEntities)
|
2016-06-12 16:54:13 +01:00
|
|
|
|
{
|
2016-06-12 20:52:49 +02:00
|
|
|
|
var entityContent = contentCache.GetById(entity.Id);
|
|
|
|
|
|
if (entityContent == null) continue;
|
2019-02-04 21:03:45 +01:00
|
|
|
|
|
|
|
|
|
|
// get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
|
2019-06-06 16:54:00 +02:00
|
|
|
|
var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray()
|
2019-02-04 21:03:45 +01:00
|
|
|
|
?? new[] {(string) null};
|
2016-06-12 20:52:49 +02:00
|
|
|
|
foreach (var x in entityContent.DescendantsOrSelf())
|
2016-06-12 16:54:13 +01:00
|
|
|
|
{
|
2019-02-04 21:03:45 +01:00
|
|
|
|
// if this entity defines specific cultures, use those instead of the default ones
|
2019-06-06 16:54:00 +02:00
|
|
|
|
var cultures = x.Cultures.Any() ? x.Cultures.Keys : defaultCultures;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-06-12 20:52:49 +02:00
|
|
|
|
|
|
|
|
|
|
LockedEvents = true; // we only want to see the "first batch"
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
2016-06-12 20:52:49 +02:00
|
|
|
|
|
2016-07-08 17:58:01 +02:00
|
|
|
|
private static void ContentService_Published(IContentService sender, PublishEventArgs<IContent> e)
|
2016-06-12 20:52:49 +02:00
|
|
|
|
{
|
|
|
|
|
|
// look note in CacheUpdated
|
|
|
|
|
|
// we might want to set a flag on the entities we are seeing here
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ContentService_Moving(IContentService sender, MoveEventArgs<IContent> e)
|
|
|
|
|
|
{
|
2019-01-27 01:17:32 -05:00
|
|
|
|
// TODO: Use the new e.EventState to track state between Moving/Moved events!
|
2016-06-12 20:52:49 +02:00
|
|
|
|
Moving = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ContentService_Moved(IContentService sender, MoveEventArgs<IContent> e)
|
|
|
|
|
|
{
|
|
|
|
|
|
Moving = false;
|
|
|
|
|
|
LockedEvents = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-04 11:21:46 +01:00
|
|
|
|
private static void CreateRedirect(int contentId, string culture, Guid contentKey, string oldRoute)
|
2016-06-12 20:52:49 +02:00
|
|
|
|
{
|
2019-04-22 18:14:03 +02:00
|
|
|
|
var contentCache = Current.UmbracoContext.Content;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
var newRoute = contentCache.GetRouteById(contentId, culture);
|
2016-06-12 20:52:49 +02:00
|
|
|
|
if (IsNotRoute(newRoute) || oldRoute == newRoute) return;
|
2016-09-01 19:06:08 +02:00
|
|
|
|
var redirectUrlService = Current.Services.RedirectUrlService;
|
2019-01-04 11:21:46 +01:00
|
|
|
|
redirectUrlService.Register(oldRoute, contentKey, culture);
|
2016-06-12 20:52:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsNotRoute(string route)
|
|
|
|
|
|
{
|
|
|
|
|
|
// null if content not found
|
|
|
|
|
|
// err/- if collision or anomaly or ...
|
|
|
|
|
|
return route == null || route.StartsWith("err/");
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
2019-01-04 11:21:46 +01:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2016-06-12 16:54:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|