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.Publishing ;
using Umbraco.Core.Events ;
using Umbraco.Web.Routing ;
using System.Collections.Generic ;
using System.Linq ;
using Umbraco.Core.Cache ;
2016-08-03 17:54:21 +02:00
using Umbraco.Core.Configuration ;
2016-07-04 18:22:06 +02:00
using Umbraco.Core.Models.PublishedContent ;
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-06-12 20:52:49 +02:00
protected override void ApplicationStarting ( UmbracoApplicationBase umbracoApplication , ApplicationContext applicationContext )
2016-06-12 16:54:13 +01:00
{
2016-08-03 17:54:21 +02:00
if ( UmbracoConfig . For . UmbracoSettings ( ) . WebRouting . DisableRedirectUrlTracking )
2016-06-12 20:52:49 +02:00
{
2016-07-04 18:22:06 +02:00
ContentFinderResolver . Current . RemoveType < ContentFinderByRedirectUrl > ( ) ;
2016-08-03 17:54:21 +02:00
}
else
{
// if any of these dlls are loaded we don't want to run our finder
var dlls = new [ ]
{
"InfoCaster.Umbraco.UrlTracker" ,
"SEOChecker" ,
"Simple301" ,
"Terabyte.Umbraco.Modules.PermanentRedirect" ,
"CMUmbracoTools" ,
"PWUrlRedirect"
} ;
// assuming all assemblies have been loaded already
// check if any of them matches one of the above dlls
var found = AppDomain . CurrentDomain . GetAssemblies ( )
. Select ( x = > x . FullName . Split ( ',' ) [ 0 ] )
. Any ( x = > dlls . Contains ( x ) ) ;
if ( found )
ContentFinderResolver . Current . RemoveType < ContentFinderByRedirectUrl > ( ) ;
}
2016-06-12 20:52:49 +02:00
}
2016-06-12 16:54:13 +01:00
2016-07-04 18:22:06 +02:00
/// <inheritdoc />
2016-06-12 20:52:49 +02:00
protected override void ApplicationStarted ( UmbracoApplicationBase umbracoApplication , ApplicationContext applicationContext )
{
// 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
//
// 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 ;
PageCacheRefresher . CacheUpdated + = PageCacheRefresher_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-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
private static void ContentService_Publishing ( IPublishingStrategy 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-04 18:22:06 +02:00
var wk = UnwrapToKey ( x ) ;
if ( wk = = null ) continue ;
OldRoutes [ x . Id ] = Tuple . Create ( wk . 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-04 18:22:06 +02:00
private static IPublishedContentWithKey UnwrapToKey ( IPublishedContent content )
{
if ( content = = null ) return null ;
var withKey = content as IPublishedContentWithKey ;
if ( withKey ! = null ) return withKey ;
var extended = content as PublishedContentExtended ;
while ( extended ! = null )
extended = ( content = extended . Unwrap ( ) ) as PublishedContentExtended ;
withKey = content as IPublishedContentWithKey ;
return withKey ;
}
2016-06-12 20:52:49 +02:00
private void PageCacheRefresher_CacheUpdated ( PageCacheRefresher sender , CacheRefresherEventArgs cacheRefresherEventArgs )
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
var removeKeys = new List < int > ( ) ;
foreach ( var oldRoute in OldRoutes )
2016-06-12 16:54:13 +01:00
{
2016-06-12 20:52:49 +02:00
// 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
2016-07-04 18:22:06 +02:00
CreateRedirect ( oldRoute . Key , oldRoute . Value . Item1 , oldRoute . Value . Item2 ) ;
2016-06-12 20:52:49 +02:00
removeKeys . Add ( oldRoute . Key ) ;
2016-06-12 16:54:13 +01:00
}
2016-06-12 20:52:49 +02:00
foreach ( var k in removeKeys )
OldRoutes . Remove ( k ) ;
}
private static void ContentService_Published ( IPublishingStrategy sender , PublishEventArgs < IContent > e )
{
// 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 ;
var redirectUrlService = ApplicationContext . 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
}
}
}