2016-06-12 16:54:13 +01:00
using System ;
using Umbraco.Core ;
using Umbraco.Core.Models ;
using Umbraco.Core.Services ;
2016-06-12 20:52:49 +02:00
using Umbraco.Core.Events ;
using System.Collections.Generic ;
using Umbraco.Core.Cache ;
2016-07-04 18:22:06 +02:00
using Umbraco.Core.Models.PublishedContent ;
2016-07-20 12:38:57 +02:00
using Umbraco.Core.Sync ;
2016-06-12 20:52:49 +02:00
using Umbraco.Web.Cache ;
2016-06-12 16:54:13 +01:00
namespace Umbraco.Web.Redirects
{
2016-07-04 18:22:06 +02:00
/// <summary>
/// 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>
/// </remarks>
2016-06-12 16:54:13 +01:00
public class RedirectTrackingEventHandler : ApplicationEventHandler
{
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" ;
2016-07-04 18:22:06 +02:00
/// <inheritdoc />
2016-09-01 19:06:08 +02:00
protected override void ApplicationStarted ( UmbracoApplicationBase umbracoApplication )
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
//
// this is all verrrry weird but it seems to work
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-07-20 12:38:57 +02:00
ContentCacheRefresher . CacheUpdated + = ContentCacheRefresher_CacheUpdated ;
2016-06-12 16:54:13 +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
2016-07-20 12:38:57 +02:00
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 )
throw new InvalidOperationException ( "ContentCacheRefresher MessageObject should be JsonPayload[]." ) ;
// manage routes
var removeKeys = new List < int > ( ) ;
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 ) ;
removeKeys . Add ( oldRoute . Key ) ;
}
foreach ( var k in removeKeys )
OldRoutes . Remove ( k ) ;
}
2016-07-04 18:22:06 +02:00
private static Dictionary < int , Tuple < Guid , string > > OldRoutes
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
get
2016-06-12 16:54:13 +01:00
{
2016-07-04 18:22:06 +02:00
var oldRoutes = ( Dictionary < int , Tuple < Guid , string > > ) UmbracoContext . Current . HttpContext . Items [ ContextKey3 ] ;
2016-06-12 20:52:49 +02:00
if ( oldRoutes = = null )
2016-07-04 18:22:06 +02:00
UmbracoContext . Current . HttpContext . Items [ ContextKey3 ] = oldRoutes = new Dictionary < int , Tuple < Guid , string > > ( ) ;
2016-06-12 20:52:49 +02:00
return oldRoutes ;
2016-06-12 16:54:13 +01:00
}
}
2016-06-12 20:52:49 +02:00
private static bool LockedEvents
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
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 ) ;
}
}
2016-06-12 16:54:13 +01:00
2016-06-12 20:52:49 +02:00
private static bool Moving
{
get { return UmbracoContext . Current . HttpContext . Items [ ContextKey1 ] ! = null ; }
set
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
if ( value )
UmbracoContext . Current . HttpContext . Items [ ContextKey1 ] = true ;
else
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
UmbracoContext . Current . HttpContext . Items . Remove ( ContextKey1 ) ;
UmbracoContext . Current . HttpContext . Items . Remove ( ContextKey2 ) ;
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 ;
var contentCache = UmbracoContext . Current . ContentCache ;
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 ;
foreach ( var x in entityContent . DescendantsOrSelf ( ) )
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
var route = contentCache . GetRouteById ( x . Id ) ;
if ( IsNotRoute ( route ) ) continue ;
2016-07-21 11:07:25 +02:00
OldRoutes [ x . Id ] = Tuple . Create ( 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 )
{
Moving = true ;
}
private static void ContentService_Moved ( IContentService sender , MoveEventArgs < IContent > e )
{
Moving = false ;
LockedEvents = false ;
}
2016-07-04 18:22:06 +02:00
private static void CreateRedirect ( int contentId , Guid contentKey , string oldRoute )
2016-06-12 20:52:49 +02:00
{
var contentCache = UmbracoContext . Current . ContentCache ;
var newRoute = contentCache . GetRouteById ( contentId ) ;
if ( IsNotRoute ( newRoute ) | | oldRoute = = newRoute ) return ;
2016-09-01 19:06:08 +02:00
var redirectUrlService = Current . Services . RedirectUrlService ;
2016-07-04 18:22:06 +02:00
redirectUrlService . Register ( oldRoute , contentKey ) ;
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
}
}
}